# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

module Buildr #:nodoc:
  # Methods added to Project to allow checking the build.
  module Checks

    module Matchers #:nodoc:

      class << self

        # Define matchers that operate by calling a method on the tested object.
        # For example:
        #   foo.should contain(bar)
        # calls:
        #   foo.contain(bar)
        def match_using(*names)
          names.each do |name|
            matcher = Class.new do
              # Initialize with expected arguments (i.e. contain(bar) initializes with bar).
              define_method(:initialize) { |*args| @expects = args }
              # Matches against actual value (i.e. foo.should exist called with foo).
              define_method(:matches?) do |actual|
                @actual = actual
                return actual.send("#{name}?", *@expects) if actual.respond_to?("#{name}?")
                return actual.send(name, *@expects) if actual.respond_to?(name)
                raise "You can't check #{actual}, it doesn't respond to #{name}."
              end
              # Some matchers have arguments, others don't, treat appropriately.
              define_method :failure_message do
                args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
                "Expected #{@actual} to #{name}#{args}"
              end
              define_method :negative_failure_message do
                args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
                "Expected #{@actual} to not #{name}#{args}"
              end
            end
            # Define method to create matcher.
            define_method(name) { |*args| matcher.new(*args) }
          end
        end

      end

      # Define delegate matchers for exist and contain methods.
      match_using :exist, :contain

    end


    # An expectation has subject, description and block. The expectation is validated by running the block,
    # and can access the subject from the method #it. The description is used for reporting.
    #
    # The expectation is run by calling #run_against. You can share expectations by running them against
    # different projects (or any other context for that matter).
    #
    # If the subject is missing, it is set to the argument of #run_against, typically the project itself.
    # If the description is missing, it is set from the project. If the block is missing, the default behavior
    # prints "Pending" followed by the description. You can use this to write place holders and fill them later.
    class Expectation

      attr_reader :description, :subject, :block

      # :call-seq:
      #   initialize(subject, description?) { .... }
      #   initialize(description?) { .... }
      #
      # First argument is subject (returned from it method), second argument is description. If you omit the
      # description, it will be set from the subject. If you omit the subject, it will be set from the object
      # passed to run_against.
      def initialize(*args, &block)
        @description = args.pop if String === args.last
        @subject = args.shift
        raise ArgumentError, 'Expecting subject followed by description, and either one is optional. Not quite sure what to do with this list of arguments.' unless args.empty?
        @block = block || lambda { |klass| info "Pending: #{description}" }
      end

      # :call-seq:
      #   run_against(context)
      #
      # Runs this expectation against the context object. The context object is different from the subject,
      # but used as the subject if no subject specified (i.e. returned from the it method).
      #
      # This method creates a new context object modeled after the context argument, but a separate object
      # used strictly for running this expectation, and used only once. The context object will pass methods
      # to the context argument, so you can call any method, e.g. package(:jar).
      #
      # It also adds all matchers defined in Buildr and RSpec, and two additional methods:
      # * it() -- Returns the subject.
      # * description() -- Returns the description.
      def run_against(context)
        subject = @subject || context
        description = @description ? "#{subject} #{@description}" : subject.to_s
        # Define anonymous class and load it with:
        # - All instance methods defined in context, so we can pass method calls to the context.
        # - it() method to return subject, description() method to return description.
        # - All matchers defined by Buildr and RSpec.
        klass = Class.new
        klass.instance_eval do
          context.class.instance_methods.each do |method|
            define_method(method) { |*args| context.send(method, *args) } unless instance_methods.include?(method)
          end
          define_method(:it) { subject }
          define_method(:description) { description }
          include ::RSpec::Matchers
          include Matchers
        end

        # Run the expectation. We only print the expectation name when tracing (to know they all ran),
        # or when we get a failure.
        begin
          trace description
          klass.new.instance_eval &@block
        rescue Exception=>error
          raise error.exception("#{description}\n#{error}").tap { |wrapped| wrapped.set_backtrace(error.backtrace) }
        end
      end

    end


    include Extension

    before_define(:check => :package) do |project|
      # The check task can do any sort of interesting things, but the most important is running expectations.
      project.task("check") do |task|
        project.expectations.inject(true) do |passed, expect|
          begin
            expect.run_against project
            passed
          rescue Exception=>ex
            if verbose
              error ex
              error ex.backtrace.select { |line| line =~ /#{Buildr.application.buildfile}/ }.join("\n")
            end
            false
          end
        end or fail "Checks failed for project #{project.name} (see errors above)."
      end
      project.task('package').enhance do |task|
        # Run all actions before checks.
        task.enhance { project.task('check').invoke }
      end
    end


    # :call-seq:
    #    check(description) { ... }
    #    check(subject, description) { ... }
    #
    # Adds an expectation. The expectation is run against the project by the check task, executed after packaging.
    # You can access any package created by the project.
    #
    # An expectation is written using a subject, description and block to validate the expectation. For example:
    #
    # For example:
    #   check package(:jar), "should exist" do
    #     it.should exist
    #   end
    #   check package(:jar), "should contain a manifest" do
    #     it.should contain("META-INF/MANIFEST.MF")
    #   end
    #   check package(:jar).path("com/acme"), "should contain classes" do
    #     it.should_not be_empty
    #   end
    #   check package(:jar).entry("META-INF/MANIFEST"), "should be a recent license" do
    #     it.should contain(/Copyright (C) 2007/)
    #   end
    #
    # If you omit the subject, the project is used as the subject. If you omit the description, the subject is
    # used as description.
    #
    # During development you can write placeholder expectations by omitting the block. This will simply report
    # the expectation as pending.
    def check(*args, &block)
      Buildr.ensure_rspec('check() method invoked in buildfile')
      expectations << Checks::Expectation.new(*args, &block)
    end

    # :call-seq:
    #   expectations() => Expectation*
    #
    # Returns a list of expectations (see #check).
    def expectations()
      @expectations ||= []
    end

  end

end


module Rake #:nodoc:
  class FileTask

    # :call-seq:
    #   exist?() => boolean
    #
    # Returns true if this file exists.
    def exist?()
      File.exist?(name)
    end

    # :call-seq:
    #   empty?() => boolean
    #
    # Returns true if file/directory is empty.
    def empty?()
      File.directory?(name) ? Dir.glob("#{name}/*").empty? : File.read(name).empty?
    end

    # :call-seq:
    #   contain?(pattern*) => boolean
    #   contain?(file*) => boolean
    #
    # For a file, returns true if the file content matches against all the arguments. An argument may be
    # a string or regular expression.
    #
    # For a directory, return true if the directory contains the specified files. You can use relative
    # file names and glob patterns (using *, **, etc).
    def contain?(*patterns)
      if File.directory?(name)
        patterns.map { |pattern| "#{name}/#{pattern}" }.all? { |pattern| !Dir[pattern].empty? }
      else
        contents = File.read(name)
        patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
          all? { |pattern| contents =~ pattern }
      end
    end

  end
end

class Buildr::Project #:nodoc:
  include Buildr::Checks
end
