

require 'active_support'
require 'active_support/core_ext/object'

module Buildwise
  class App
    module ParallelHelper

      def current_active_distrbuted_build
        @build = Build.where(:status => "building", :is_parallel => true).first
        return @build
      end
      
      def exceeds_load_testing_time_limit?(a_build)
        @project = a_build.project
        if @project && @project.config.is_load_testing && @project.config.load_max_duration > 0
          build_start_time = a_build.start_time
          if Time.now > (build_start_time + @project.config.load_max_duration * 60)
            logger.info("[Load] max execution time reached, remove unassigned test files")

            unrun_test_files = TestFile.where(:build_id => a_build.id).where("agent IS NULL")
            logger.info("[Load] #{unrun_test_files.size} test files removed")
            unrun_test_files.each do |utf|
              utf.destroy
            end
            return true        
          end          
        end
        return false
      end
    
      
      def exceeds_load_testing_agent_limit?(a_build, agent_name)
        if a_build.is_load_testing && a_build.load_max_agent_count > 0

          this_build_agent_names = TestFile.where(:build_id => a_build.id).pluck("agent").uniq.compact
          logger.info("{Load} check agent limit: #{agent_name} against  #{this_build_agent_names.inspect}")          
          return this_build_agent_names.size >= a_build.load_max_agent_count && !this_build_agent_names.include?(agent_name)
        end
        return false
      end
    
    
      def handle_load_test_results(build_id = nil, test_result_hash = {})
        start_time = Time.now
        @build = Build.find_by_id(build_id)
        @build ||=  Build.where(:status => "building", :is_parallel => true, :is_load_testing => true).first
           
        if @build.nil? || !@build.incomplete?
          logger.info("Invalid build for load results\n\n\n")
          puts("\n\nWarn: @build is #{build_id} or already cmpelte")
          return 
        end
        
        timings_json = test_result_hash["timings_json"]
        timings_json_obj = JSON.parse(timings_json)        
        agent_name = test_result_hash["agent_name"]
        
        ret = receive_load_test_results_from_agent(@build.id, agent_name, timings_json_obj)    
        body ret
      end    
    
    
      def handle_test_results(build_id = nil, test_result_hash = {})
        start_time = Time.now

        if build_id.nil?
          @build = Build.find_by_id(build_id)
        elsif current_active_distrbuted_build
          @build = current_active_distrbuted_build
        else
          logger.warn("No valid build (nil) found in handle_test_results()")
          return # not valid build to process results
        end

        begin
          spec_id = test_result_hash[:spec_id]
          spec_file = test_result_hash[:spec_file]
          result_xml = test_result_hash[:result_xml]
          agent_ip_address = test_result_hash[:ip_address]
          browser = test_result_hash[:browser]


          screenshot_zip = test_result_hash[:screenshot_zip]    
        
          test_report_dir = File.join(BUILDWISE_ROOT, "log", "reports")
          FileUtils.mkdir_p(test_report_dir) unless File.exist?(test_report_dir)
          filename = spec_id.to_s + "_" + agent_ip_address + "_" + File.basename(spec_file.to_s) + ".xml"
          File.open(File.join(test_report_dir, filename), "w").write(result_xml)

          begin
            has_screenshot = false
            if screenshot_zip && screenshot_zip[:tempfile] && File.exist?(screenshot_zip[:tempfile])
              temp_file = screenshot_zip[:tempfile]
              build_artifact_folder_name = build_id.to_s.rjust(5, "0")      
              test_file_artifact_folder_name = spec_id.to_s.rjust(5, "0")      
              build_spec_artifact_dir = File.join(BUILDWISE_ROOT, "public","artifacts", build_artifact_folder_name, test_file_artifact_folder_name)
              unless File.exist?(build_spec_artifact_dir)
                FileUtils.mkdir_p(build_spec_artifact_dir)
              end
              target_screenshot_zip_file = File.join(build_spec_artifact_dir, "screenshots.zip")      


              FileUtils.cp(temp_file.path, target_screenshot_zip_file)
          
              Archive::Zip.extract(target_screenshot_zip_file, build_spec_artifact_dir)
              FileUtils.rm(target_screenshot_zip_file)
              has_screenshot = true
            end
          rescue => e
            logger.warn("Failed to handle screenshot!!, ignore for save the result.")
          end
          
        
          if result_xml.include?("WIN32OLERuntimeError")
            logger.warn("Trouble agents: #{agent_ip_address} | IP: #{@env['REMOTE_ADDR']}")
            settings.troubled_agents[agent_ip_address] = {:raised_at => Time.now, :ip_address => @env['REMOTE_ADDR'], :expired_at => Time.now + 3 * 60}
          end
        

          existing_test_file = TestFile.find_by_id(spec_id.to_i) rescue nil
          if existing_test_file && existing_test_file.test_cases.any?
            logger.info("Clear previous #{existing_test_file.filename}'s #{existing_test_file.test_cases.count} test cases")
            existing_test_file.test_cases.destroy_all
          end
        
          ret = receive_functional_test_results_from_agent(spec_id, spec_file, result_xml, browser, has_screenshot)    
          logger.info("[#{agent_ip_address}] Receive test results: #{spec_file}. #{Time.now - start_time} => #{ret} on browser => #{browser}")
          body ret
        rescue => e
          logger.error("Failed to handle_test_results() for build: #{build_id}, #{e}")


          logger.info("Test result data: #{test_result_hash.inspect}")
          body "ERROR"
        end
      end

      def receive_functional_test_results_from_agent(spec_id, spec_file, result_xml, browser, has_screenshot = false)
        begin
          @test_file = TestFile.find_by_id(spec_id.to_i)
          return "Error" if @test_file.nil?
          logger.info("Receive agent results for file: #{@test_file.id}, on browser #{browser}, result => #{@test_file.result}")
          if @test_file.result == "OK" && @test_file.test_cases.size > 0
            return "OK" # already pass
          end

          if result_xml

            if @test_file.test_cases.any? then
              @test_file.test_cases.each do |tc|
                tc.destroy
              end
            end

            if result_xml =~ /^error/i
              @test_file.num_total = 0
              @test_file.num_failures = 0
              @test_file.num_errors = 0
              @test_file.successful = false
              @test_file.duration = 0
              @test_file.result = "Error"
              @test_file.comments = result_xml
                        
            elsif result_xml =~ /^..TEST_LOAD_ERROR/

                @test_file.num_total = 0
                @test_file.num_failures = 0
                @test_file.num_errors = 0
                @test_file.successful = false
                @test_file.duration = 0
                @test_file.result = "Error"
                @test_file.comments = result_xml[0..2048]
            else
            
              @test_file.test_cases.destroy_all # avoid duplicate test cases
              parse_junit_result_nokogiri(@test_file, result_xml, browser)

            end
          end
          
          if has_screenshot
            build_artifact_folder_name = @test_file.build.id.to_s.rjust(5, "0")
            test_file_artifact_folder_name = @test_file.id.to_s.rjust(5, "0")
            build_spec_artifact_dir = File.join(BUILDWISE_ROOT, "public","artifacts", build_artifact_folder_name, test_file_artifact_folder_name)
            @test_file.test_cases.reload
            
            suite_name = @test_file.description
            @test_file.test_cases.each do |tc|          
              check_screenshot_file = File.join(build_spec_artifact_dir, "screenshot-#{tc.name.parameterize}.png")
              if !File.exist?(check_screenshot_file) && suite_name                
                check_screenshot_file = File.join(build_spec_artifact_dir, "screenshot-" + suite_name.parameterize + "_" + tc.name.parameterize + ".png")                  
              end              
              
              if File.exist?(check_screenshot_file)
                saved_test_case_screenshot_file =  File.join(build_spec_artifact_dir, "screenshot-#{tc.id}.png")
                FileUtils.mv(check_screenshot_file, saved_test_case_screenshot_file)
                tc.update_column(:screenshot, saved_test_case_screenshot_file.gsub("#{BUILDWISE_ROOT}/public", ""))
              end

            end
          end

        rescue => e
          logger.warn("error: on parse #{spec_file} result xml: #{e}, #{e.backtrace}")
          raise e unless @test_file 
          if @test_file && @test_file.past_agents.nil? && @test_file.agent then
            give_one_more_chance(@test_file, "Exception #{e} on #{@test_file.agent}")
          else

            @test_file.result = "Error"
            @test_file.comments = e
          end

          logger.info("#{Time.now} [Error] Failed to report test result: #{e}, #{e.backtrace}")
          return "Error: #{e}"
        ensure


          @test_file.save! if @test_file
        end



        if @test_file && @test_file.test_cases.size == 0 && @test_file.result == "OK"
          logger.info(" ^^^ test_file: #{@test_file.id} saved, but no test cases. \n|#{result_xml}|")
        end
        
        @build ||= @test_file.build
        @build.refresh_ui_test_status

        return "OK"
      end


      def receive_load_test_results_from_agent(build_id, agent_name, timings_json_obj)
        begin
          @build = Build.find_by_id(build_id.to_i)
          return "Error" if @build.nil?

          timings_json_obj.each do |jo|
            new_load_test_result = LoadTestResult.new 
            new_load_test_result.build_id = build_id
            new_load_test_result.agent_name = agent_name
            new_load_test_result.operation =  jo["operation"]
            new_load_test_result.start_timestamp = jo["start_ts"]
            new_load_test_result.end_timestamp = new_load_test_result.start_timestamp + jo["duration"]
            new_load_test_result.duration = jo["duration"]
            new_load_test_result.success = jo["success"] == 1 || jo["success"] == true
            new_load_test_result.error = jo["error"]
            new_load_test_result.server_digest = SERVER_DIGEST if defined?(SERVER_DIGEST)
            new_load_test_result.save    
          end
              
        rescue => e
          logger.info("#{Time.now} [Error] Failed to report load test result: #{e}, #{e.backtrace}")
          return "Error: #{e}"
        end
        return "OK"
      end
      



      def parse_junit_result_nokogiri(test_file, result_xml, browser)
        doc = Nokogiri.XML(result_xml)
        suite_count = doc.xpath("/testsuites/testsuite").count
        first_test_suite = doc.xpath("/testsuites/testsuite").first
        
        if suite_count == 1
          begin      
            first_test_suite.attributes # if not valid
          rescue => e
            first_test_suite = doc.root
          end
        else

          second_test_suite = doc.xpath("/testsuites/testsuite")[1]
          
          if first_test_suite && second_test_suite && first_test_suite["tests"].to_s == "0" && second_test_suite["tests"].to_s.to_i > 0
            first_test_suite = doc.xpath("/testsuites/testsuite")[1]
          end
          
          begin      
            first_test_suite.attributes # if not valid
          rescue => e
            first_test_suite = doc.root
          end
        
        end  
        
        suite_name, test_count, duration, test_failures, test_errors = first_test_suite.attributes['name'].to_s, first_test_suite.attributes['tests'].to_s, first_test_suite.attributes['time'].to_s.to_f, first_test_suite.attributes['failures'].to_s, first_test_suite.attributes['errors'].to_s

        test_file.description = suite_name
        test_file.num_total = test_count.to_i
        test_file.num_failures = test_failures.to_i
        test_file.num_errors = test_errors.to_i
        
        test_file.successful = test_file.num_total > 0 && (test_file.num_errors && test_file.num_errors == 0) && (test_file.num_failures && test_file.num_failures == 0)
        test_file.duration = duration.to_f
        test_file.browser = browser

        if test_file.successful then
          test_file.result = "OK"
        else
          if test_file.result != "Failed" && test_file.past_agents.blank? then
            give_one_more_chance(test_file, "Error: #{test_file.num_failures} | Failure: #{test_file.num_failures} on #{test_file.agent}")
          else
            test_file.result = "Failed"
          end
        end

        test_file.stdout = doc.xpath("//system-out").text rescue nil    
        test_file.stderr = doc.xpath("//system-err").text rescue nil    

        doc.xpath("//testcase").each { |element|
          test_case_name =  element.attributes["name"].to_s
          test_case = TestCase.new(:name => test_case_name, :duration => element.attributes["time"].to_s.to_f, :created_at => Time.now)
          
          if test_file && suite_name && suite_name.strip.size >= 3 && test_file.build.project.config.remove_suite_name_from_test_case        
            test_case.remove_prefix(suite_name)
          end
          
          failure_nodes = element.elements.select { |x| x.name == "failure" }
          error_nodes = element.elements.select { |x| x.name == "error" }

          if failure_nodes.any? then
            test_case.failure = failure_nodes.first.text
            test_case.failure = test_case.failure.force_encoding("UTF-8")
            test_case.successful = false
          elsif error_nodes.any?
            test_case.failure = error_nodes.first.text
            test_case.successful = false
          else
            test_case.successful = true
          end

          test_case.test_file_id = test_file.id
          test_case.build_id = test_file.build_id
          test_case.project_id = test_file.build.project_id
          test_case.save!
          test_file.test_cases << test_case
        }
      end



      def allow_agent_for_the_build(agent_ip_address, a_build)


        if agent_ip_address.nil?

          return false
        end

        agent_ip_address.strip!

        begin
          the_project = a_build.project

          if a_build.is_load_testing  && exceeds_load_testing_agent_limit?(a_build, agent_ip_address)
            return false
          end

          the_project_default_agents = the_project.config.default_agents
          if the_project_default_agents.nil? || the_project_default_agents.empty?

            return true
          end
                    


          return the_project_default_agents.include?(agent_ip_address)
        rescue => e
          logger.warn("Failed to allow_agent_for_the_build, #{e}. Ignore, allow")
          return false if a_build.is_load_testing
          puts "Failed to check allow_agent_for_the_build, #{e}. Ignore, allow for functional testing"
          return true
        end

      end




      def check_for_long_running_tests
        assigned_tests = @build.test_files.find(:all, :conditions => ["status = ? || status = ?", "In progress", "Pending"])
        assigned_tests.each do |one_test|
          duration = Time.now - one_test.assigned_at
          logger.info("long-running tests: #{one_test} => #{duration}") if duration > 200
          if duration > 1000
            give_one_more_chance(one_test, "takes too long")
          end
        end
      end


      def give_one_more_chance(test_file, comments)
        return if test_file.nil?
        retry_on_another_machine = test_file.build.project.config(false).retry_on_another_machine rescue false
        logger.info("Giving one more chance for test #{test_file.filename}  => #{retry_on_another_machine}")
        if retry_on_another_machine
          test_file.past_agents = test_file.agent
          test_file.test_cases.each do |tc|
            tc.destroy
          end
          test_file.agent = nil
          test_file.status = nil
          test_file.result = nil
          test_file.comments = test_file.comments || "" + "; " + comments
          test_file.save
        else
          test_file.result = "Failed"
          test_file.save
        end
      end


    end

    helpers ParallelHelper
  end
end