require 'custodian/settings'
require 'uri'


#
#  The HTTP-protocol test.
#
#  This object is instantiated if the parser sees a line such as:
#
###
### http://foo.vm.bytemark.co.uk/ must run http with content 'page text' otherwise 'http fail'.
###
#
#
module Custodian

  module ProtocolTest


    class HTTPTest < TestFactory

      #
      # The line from which we were constructed.
      #
      attr_reader :line

      #
      # The URL to poll
      #
      attr_reader :url

      #
      # The expected status + content
      #
      attr_reader :expected_status, :expected_content

      #
      # Constructor
      #
      def initialize( line )

        #
        #  Save the line
        #
        @line = line

        #
        #  Save the URL
        #
        @url  = line.split( /\s+/)[0]
        @host = @url


        #
        # Will we follow redirects?
        #
        @redirect = true


        #
        #  Ensure we've got a HTTP/HTTPS url.
        #
        if ( @url !~ /^https?:/ )
          raise ArgumentError, "The target wasn't a HTTP/HTTPS URL: #{line}"
        end


        #
        # Determine that the protocol of the URL matches the
        # protocol-test we're running
        #
        test_type = nil

        case line
        when /\s+must\s(not\s+)?run\s+http(\s+|\.|$)/i
        then
          test_type = "http"
        when /\s+must\s+(not\s+)?run\s+https(\s+|\.|$)/i
        then
          test_type = "https"
        else
          raise ArgumentError, "URL has invalid scheme: #{@line}"
        end

        #
        #  Get the schema of the URL
        #
        u = URI.parse( @url )
        if ( u.scheme != test_type )
          raise ArgumentError, "The test case has a different protocol in the URI than that which we're testing: #{@line} - \"#{test_type} != #{u.scheme}\""
        end


        #
        # Is this test inverted?
        #
        if ( line =~ /must\s+not\s+run\s+/ )
          @inverted = true
        else
          @inverted = false
        end

        #
        # Expected status
        #
        if ( line =~ /with status ([0-9]+)/ )
          @expected_status = $1.dup
        else
          @expected_status = "200"
        end

        #
        # The content we expect to find
        #
        if ( line =~ /with content '([^']+)'/ )
          @expected_content = $1.dup
        else
          @expected_content = nil
        end

        #
        # Do we follow redirects?
        #
        if ( line =~ /not following redirects?/i )
          @redirect = false
        end

        #
        # Do we use cache-busting?
        #
        @cache_busting = true
        if ( line =~ /with\s+cache\s+busting/ )
          @cache_busting = true
        end
        if ( line =~ /without\s+cache\s+busting/ )
          @cache_busting = false
        end
      end


      #
      #  Get the right type of this object, based on the URL
      #
      def get_type
        if ( @url =~ /^https:/ )
          "https"
        elsif ( @url =~ /^http:/ )
          "http"
        else
          raise ArgumentError, "URL isn't http/https: #{@url}"
        end
      end


      #
      #  Do we follow redirects?
      #
      def follow_redirects?
        @redirect
      end

      #
      #  Do we have cache-busting?
      #
      def cache_busting?
        @cache_busting
      end

      #
      # Allow this test to be serialized.
      #
      def to_s
        @line
      end



      #
      # Run the test.
      #
      def run_test

        #  Reset state, in case we've previously run.
        @error    = nil

        begin
          require 'rubygems'
          require 'curb'
        rescue LoadError
          @error = "The required rubygem 'curb' was not found."
          return false
        end

        #
        # Get the timeout period for this test.
        #
        settings = Custodian::Settings.instance()
        period   = settings.timeout()

        #
        # The URL we'll fetch/poll.
        #
        test_url = @url

        #
        #  Parse and append a query-string if not present, if we're
        # running with cache-busting.
        #
        if ( @cache_busting )
          u = URI.parse( test_url )
          if ( ! u.query )
            u.query   = "ctime=#{Time.now.to_i}"
            test_url  = u.to_s
          end
        end

        errors = []

        %(:ipv4 ipv6).each do |resolve_mode|
          status   = nil
          content  = nil

          c = Curl::Easy.new(test_url)

          c.resolve_mode = resolve_mode

          #
          # Should we follow redirections?
          #
          if ( follow_redirects? )
            c.follow_location = true
            c.max_redirects   = 10
          end

          c.ssl_verify_host = false
          c.ssl_verify_peer = false
          c.timeout         = period

          #
          # Set a basic protocol message, for use later.
          #
          protocol_msg =  (resolve_mode == :ipv4 : "IPv4" ? "IPv6")

          begin
            timeout( period ) do
              c.perform
              status = c.response_code
              content = c.body_str
            end

            #
            # Overwrite protocol_msg with the IP we connect to. 
            #
            if c.primary_ip
              if :ipv4 == resolve_mode
                protocol_msg = "#{c.primary_ip}" 
              else
                protocol_msg = "[#{c.primary_ip}]" 
              end
            end

          rescue Curl::Err::SSLCACertificateError => x
            errors << "#{protocol_msg}: SSL validation error: #{x.message}."
          rescue Curl::Err::TimeoutError, Timeout::Error
            errors << "#{protocol_msg}: Timed out fetching page."
          rescue Curl::Err::TooManyRedirectsError
            errors << "#{protocol_msg}: More than 10 redirections."
          rescue => x
            errors << "#{protocol_msg}: #{x.class.to_s}: #{x.message}."
          end
  
          #
          # A this point we've either had an exception, or we've
          # got a result
          #
          if ( status and expected_status.to_i != status.to_i )
            errors << "#{protocol_msg}: Status code was #{status} not the expected #{expected_status}."
          end
  
          if ( content.is_a?(String) and 
               expected_content.is_a?(String) and 
               content =~ /#{expected_content}/i )
            errors << "#{protocol_msg}: The response did not contain our expected text '#{expected_content}'."
          end
        end

        if errors.length > 0
          @error = errors.join("\n")
          return false
        end
  
        #
        #  All done.
        #
        return true
      end

      #
      # If the test fails then report the error.
      #
      def error
        @error
      end


      register_test_type "http"
      register_test_type "https"

    end
  end
end