require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/server/server'))
require 'test/unit'
require 'rack/test'
require 'shoulda'
require 'flexmock/test_unit'

set :environment, :test

module Testbot::Server

  class ServerTest < Test::Unit::TestCase
    include Rack::Test::Methods

    def setup
      Job.delete_all
      Runner.delete_all
      Build.delete_all
    end

    def app
      Sinatra::Application
    end

    context "POST /builds" do

      should "create a build and return its id" do
        flexmock(Runner).should_receive(:total_instances).and_return(2)
        post '/builds', :files => 'spec/models/car_spec.rb spec/models/house_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "10 20", :jruby => false

        first_build = Build.all.first
        assert last_response.ok?

        assert_equal first_build.id.to_s, last_response.body
        assert_equal 'spec/models/car_spec.rb spec/models/house_spec.rb', first_build.files
        assert_equal '10 20', first_build.sizes
        assert_equal 'server:/path/to/project', first_build.root
        assert_equal 'spec', first_build.type
        assert_equal 'things', first_build.project
        assert_equal 0, first_build.jruby
        assert_equal '', first_build.results
        assert_equal true, first_build.success
      end

      should "create jobs from the build based on the number of total instances" do
        flexmock(Runner).should_receive(:total_instances).and_return(2)      
        flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').once.and_return([
                                                                                                                                                                                                                   ["spec/models/car_spec.rb", "spec/models/car2_spec.rb"],
                                                                                                                                                                                                                   ["spec/models/house_spec.rb", "spec/models/house2_spec.rb"]
        ])

        post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true

        assert_equal 2, Job.count
        first_job, last_job = Job.all
        assert_equal 'spec/models/car_spec.rb spec/models/car2_spec.rb', first_job.files
        assert_equal 'spec/models/house_spec.rb spec/models/house2_spec.rb', last_job.files

        assert_equal 'server:/path/to/project', first_job.root
        assert_equal 'spec', first_job.type
        assert_equal 'things', first_job.project
        assert_equal 1, first_job.jruby
        assert_equal Build.all.first, first_job.build
      end

      should "return a 503 error if there are no known runners" do
        flexmock(Runner).should_receive(:total_instances).and_return(0)
        post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true
        assert_equal 0, Job.count
        assert_equal 503, last_response.status
        assert_equal "No runners available", last_response.body
      end

      should "only use resources according to available_runner_usage" do
        flexmock(Runner).should_receive(:total_instances).and_return(4)
        flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').and_return([])
        post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :sizes => "1 1 1 1", :available_runner_usage => "50%"
      end

    end

    context "GET /builds/:id" do

      should 'return the build status' do
        build = Build.create(:done => false, :results => "testbot5\n..........\ncompleted", :success => false)
        get "/builds/#{build.id}"
        assert_equal true, last_response.ok?
        assert_equal ({ "done" => false, "results" => "testbot5\n..........\ncompleted", "success" => false }),
          JSON.parse(last_response.body)
      end

      should 'remove a build that is done' do
        build = Build.create(:done => true)
        get "/builds/#{build.id}"
        assert_equal true, JSON.parse(last_response.body)['done']
        assert_equal 0, Build.count
      end

      should 'remove all related jobs of a build that is done' do
        build = Build.create(:done => true)
        related_job = Job.create(:build => build)
        other_job = Job.create(:build => nil)
        get "/builds/#{build.id}"
        assert !Job.find(related_job.id)
        assert Job.find(other_job.id)
      end

    end

    context "GET /jobs/next" do

      should "be able to return a job and mark it as taken" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 1

        get '/jobs/next', :version => Testbot.version
        assert last_response.ok?      

        assert_equal [ job1.id, build.id, "things", "server:/project", "spec", "jruby", "spec/models/car_spec.rb" ].join(','), last_response.body
        assert job1.taken_at != nil
      end

      should "not return a job that has already been taken" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now, :type => 'spec', :build => build
        job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 0
        get '/jobs/next', :version => Testbot.version
        assert last_response.ok?
        assert_equal [ job2.id, build.id, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body
        assert job2.taken_at != nil
      end

      should "not return a job if there isnt any" do
        get '/jobs/next', :version => Testbot.version
        assert last_response.ok?
        assert_equal '', last_response.body
      end

      should "save which runner takes a job" do
        job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => Build.create
        get '/jobs/next', :version => Testbot.version
        assert_equal Runner.first, job.taken_by
      end

      should "save information about the runners" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini.local', :uid => "00:01:...", :idle_instances => 2, :max_instances => 4
        runner = Runner.first
        assert_equal Testbot.version, runner.version
        assert_equal '127.0.0.1', runner.ip
        assert_equal 'macmini.local', runner.hostname
        assert_equal '00:01:...', runner.uid
        assert_equal 2, runner.idle_instances
        assert_equal 4, runner.max_instances
        assert (Time.now - 5) < runner.last_seen_at
        assert (Time.now + 5) > runner.last_seen_at
      end

      should "only create one record for the same mac" do
        get '/jobs/next', :version => Testbot.version, :uid => "00:01:..."
        get '/jobs/next', :version => Testbot.version, :uid => "00:01:..."
        assert_equal 1, Runner.count
      end

      should "not return anything to outdated clients" do
        Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project'
        get '/jobs/next', :version => "1", :uid => "00:..."
        assert last_response.ok?
        assert_equal '', last_response.body
      end

      should "only give jobs from the same source to a runner" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :build => build
        get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id

        # Creating the second job here because of the random lookup.
        job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build
        get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id + 1

        assert last_response.ok?
        assert_equal '', last_response.body
      end

      should "not give more jruby jobs to an instance that can't take more" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => build
        get '/jobs/next', :version => Testbot.version, :uid => "00:..."

        # Creating the second job here because of the random lookup.
        job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 1, :build => build
        get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true"

        assert last_response.ok?
        assert_equal '', last_response.body
      end

      should "still return other jobs when the runner cant take more jruby jobs" do
        job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => Build.create
        get '/jobs/next', :version => Testbot.version, :uid => "00:..."

        # Creating the second job here because of the random lookup.
        job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 0, :build => Build.create
        get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true"

        assert last_response.ok?
        assert_equal job2.id.to_s, last_response.body.split(',')[0]
      end

      should "return the jobs in random order in order to start working for a new build right away" do
        build1, build2 = Build.create, Build.create
        20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build1 }

        20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build2 }

        build_ids = (0...10).map {
          get '/jobs/next', :version => Testbot.version, :uid => "00:..."
          last_response.body.split(',')[1]
        }

        assert build_ids.find { |build_id| build_id == build1.id.to_s }
        assert build_ids.find { |build_id| build_id == build2.id.to_s }
      end

      should "return the jobs randomly when passing build_id" do
        build = Build.create
        20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build }

        20.times { Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build }

        files = (0...10).map {
          get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id
          last_response.body.split(',').last
        }

        assert files.find { |file| file.include?('car') }
        assert files.find { |file| file.include?('house') }
      end

      should "return taken jobs to other runners if the runner hasn't been seen for 10 seconds or more" do
        missing_runner = Runner.create(:last_seen_at => Time.now - 15)
        build = Build.create
        old_taken_job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :taken_by => missing_runner, :taken_at => Time.now - 30, :project => 'things'

        new_runner = Runner.create(:uid => "00:01")
        get '/jobs/next', :version => Testbot.version, :uid => "00:01"
        assert_equal new_runner, old_taken_job.taken_by

        assert last_response.ok?
        assert_equal [ old_taken_job.id, build.id.to_s, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body
      end

    end

    context "/runners/outdated" do

      should "return a list of outdated runners" do
        get '/jobs/next', :version => "1", :hostname => 'macmini1.local', :uid => "00:01"
        get '/jobs/next', :version => "1", :hostname => 'macmini2.local', :uid => "00:02"
        get '/jobs/next'    
        get '/jobs/next', :version => Testbot.version.to_s, :hostname => 'macmini3.local', :uid => "00:03"
        assert_equal 4, Runner.count
        get '/runners/outdated'
        assert last_response.ok?
        assert_equal "127.0.0.1 macmini1.local 00:01\n127.0.0.1 macmini2.local 00:02\n127.0.0.1", last_response.body
      end

    end

    context "GET /runners/available_runners" do

      should "return a list of available runners" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => 'user1'
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2'
        get '/runners/available'
        assert last_response.ok?
        assert_equal "127.0.0.1 macmini1.local 00:01 user1 2\n127.0.0.1 macmini2.local 00:02 user2 4", last_response.body
      end

      should "not return a runner as available when it hasnt pinged the server yet" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :username => 'user1'
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2'
        get '/runners/available'
        assert last_response.ok?
        assert_equal "127.0.0.1 macmini2.local 00:02 user2 4", last_response.body
      end

      should "not return runners as available when not seen the last 10 seconds" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => "user1"
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
        Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)      
        get '/runners/available'
        assert_equal "127.0.0.1 macmini1.local 00:01 user1 2", last_response.body
      end

    end

    context "GET /runners/available_instances" do

      should "return the number of available runner instances" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
        get '/runners/available_instances'
        assert last_response.ok?
        assert_equal "6", last_response.body
      end    

      should "not return instances as available when not seen the last 10 seconds" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
        Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
        get '/runners/available_instances'
        assert last_response.ok?
        assert_equal "2", last_response.body
      end

    end

    context "GET /runners/total_instances" do

      should "return the number of available runner instances" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2
        get '/runners/total_instances'
        assert last_response.ok?
        assert_equal "6", last_response.body
      end    

      should "not return instances as available when not seen the last 10 seconds" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2
        Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
        get '/runners/total_instances'
        assert last_response.ok?
        assert_equal "2", last_response.body
      end

    end  

    context "GET /runners/ping" do

      should "update last_seen_at for the runner" do
        runner = Runner.create(:uid => 'aa:aa:aa:aa:aa:aa')
        get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version
        assert last_response.ok?
        assert (Time.now - 5) < runner.last_seen_at
        assert (Time.now + 5) > runner.last_seen_at
      end

      should "update data on the runner" do
        build = Build.create
        runner = Runner.create(:uid => 'aa:aa:..')
        get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => build.id
        assert last_response.ok?
        assert_equal 'aa:aa:..', runner.uid
        assert_equal 4, runner.max_instances
        assert_equal 2, runner.idle_instances
        assert_equal 'hostname1', runner.hostname
        assert_equal Testbot.version, runner.version
        assert_equal 'jocke', runner.username
        assert_equal build, runner.build
      end

      should "do nothing if the version does not match" do
        runner = Runner.create(:uid => 'aa:aa:..', :version => Testbot.version)
        get "/runners/ping", :uid => 'aa:aa:..', :version => "OLD"
        assert last_response.ok?
        assert_equal Testbot.version, runner.version
      end

      should "do nothing if the runners isnt known yet found" do
        get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version
        assert last_response.ok?
      end

      should "return an order to stop the build if the build id does not exist anymore" do
        runner = Runner.create(:uid => 'aa:aa:..')
        get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => 1
        assert_equal last_response.body, "stop_build,1"
      end

      should "not return an order to stop a build without an id" do
        runner = Runner.create(:uid => 'aa:aa:..')
        get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => ''
        assert_equal last_response.body, ''
        get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => nil
        assert_equal last_response.body, ''
      end

    end

    context "PUT /jobs/:id" do

      should "receive the results of a job" do
        job = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30
        put "/jobs/#{job.id}", :result => 'test run result', :status => "successful"
        assert last_response.ok?
        assert_equal 'test run result', job.result
        assert_equal 'successful', job.status
      end

      should "update the related build" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build      
        put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful"
        put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
        assert_equal 'test run result 1\ntest run result 2\n', build.results
        assert_equal true, build.success
      end

      should "make the related build done if there are no more jobs for the build" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful"
        put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
        assert_equal true, build.done
      end

      should "make the build fail if one of the jobs fail" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "failed"
        put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
        assert_equal false, build.success
      end 

      should "be able to update from multiple result postings" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        # maybe later:
        # put "/jobs/#{job.id}", :result => 'Preparing, db setup, etc.', :status => "preparing"
        put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running"
        put "/jobs/#{job2.id}", :result => 'Running other tests. done.', :status => "successful"
        put "/jobs/#{job1.id}", :result => 'Running tests....', :status => "running"
        assert_equal false, build.done
        assert_equal false, job1.done
        assert_equal "Running tests....", job1.result
        assert_equal "Running tests..Running other tests. done...", build.results
      end

      should "not break when updating without new results" do
        build = Build.create
        job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
        put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running"
        put "/jobs/#{job1.id}", :result => '', :status => "successful"
        assert_equal "Running tests..", build.results
      end

    end

    context "GET /version" do

      should "return its version" do
        get '/version'
        assert last_response.ok?
        assert_equal Testbot.version.to_s, last_response.body
      end

    end

    context "GET /runners" do

      should "return runner information in json format" do
        get '/jobs/next', :version => Testbot.version, :uid => "00:01"
        get "/runners/ping", :uid => '00:01', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'testbot', :build_id => nil
        get '/runners'
        
        assert last_response.ok?
        assert_equal ([ { "version" => Testbot.version.to_s, "build" => nil, "hostname" => 'hostname1', "uid" => "00:01",
                          "idle_instances" => 2, "max_instances" => 4, "username" => 'testbot',
                          "ip" => "127.0.0.1", "last_seen_at" => Runner.first.last_seen_at.to_s } ]),
                     JSON.parse(last_response.body)
      end

      should "not return instances when not seen the last 10 seconds" do
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
        get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
        Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
        get '/runners'
        assert last_response.ok?
        parsed_body = JSON.parse(last_response.body)
        assert_equal 1, parsed_body.size
        assert_equal '00:01', parsed_body.first["uid"] 
      end

    end

    context "GET /status" do

      should "return the contents of the status page" do
        get '/status'
        assert_equal true, last_response.body.include?('Testbot status')
      end

    end

    context "GET /status/:dir/:file" do

      should "return the file" do
        get "/status/javascripts/jquery-1.4.4.min.js"
        assert_equal true, last_response.body.include?('jQuery JavaScript Library v1.4.4')
      end

    end

  end

end

