require 'rubygems'
require 'rack/test'
require 'shoulda'
require 'rack-raw-upload'
require 'multi_json'
require 'digest'
require 'rr'

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

  def app
    opts = @middleware_opts
    Rack::Builder.new do
      use Rack::RawUpload, opts
      run Proc.new { |env| [200, {'Content-Type' => 'text/html'}, ['success']] }
    end
  end

  def setup
    @middleware_opts = {}
    @path = File.join(File.dirname(__FILE__), %w{data me-in-shanghai.jpg})
    @filename = File.basename(@path)
    @file = File.open(@path) # Maybe use mode 'rb:ASCII-8BIT'? May help with the pending test below. Not doing it for the moment, as it's not supported by Rubinius
  end

  def upload(env = {})
    env = {
      'CONTENT_TYPE' => 'application/octet-stream',
      'rack.input' => @file,
    }.merge(env)
    do_request(env)
  end

  def post(env = {})
    env = {
      'CONTENT_TYPE' => 'multipart/form-data',
      'rack.input' => StringIO.new('things=stuff'),
    }.merge(env)
    do_request(env)
  end

  def do_request(env = {})
    input = env['rack.input']
    length = input.respond_to?(:size) ? input.size : File.size(input.path)
    env = {
      'REQUEST_METHOD' => 'POST',
      'PATH_INFO' => '/some/path',
      'CONTENT_LENGTH' => length.to_s,
    }.merge(env)
    request(env['PATH_INFO'], env)
  end

  context "raw file upload" do
    should "work with PUT requests" do
      upload('REQUEST_METHOD' => 'PUT')
      assert_file_uploaded
    end

    should "work with Content-Type 'application/octet-stream'" do
      upload
      assert_file_uploaded_as 'application/octet-stream'
    end

    should "work with Content-Type 'image/jpeg'" do
      upload('CONTENT_TYPE' => 'image/jpeg')
      assert_file_uploaded_as 'image/jpeg'
    end

    should "not work with Content-Type 'application/x-www-form-urlencoded'" do
      post('CONTENT_TYPE' => 'application/x-www-form-urlencoded')
      assert_successful_non_upload
    end

    should "not work with Content-Type 'multipart/form-data'" do
      post
      assert_successful_non_upload
    end

    should "not work when there is no input" do
      upload('rack.input' => '')
      assert_successful_non_upload

      upload('rack.input' => StringIO.new(''))
      assert_successful_non_upload
    end

    # "stuff" should be something like "boundary=----WebKitFormBoundaryeKPeU4p65YgercgO",
    # but if I do that here, Rack tries to be clever and the test breaks
    should "not work with Content-Type 'multipart/form-data; stuff'" do
      post('CONTENT_TYPE' => 'multipart/form-data; stuff')
      assert_successful_non_upload
    end

    should "work when the input is a Tempfile" do
      tempfile = Tempfile.new('rack-raw-upload-test-')
      tempfile << @file.read
      tempfile.rewind
      upload('rack.input' => tempfile)
      assert_file_uploaded
    end

    # Sould be something like this, but I can't get this to fail
    # when it should. Instead, I'm testing using the example
    # undefined_conversion_error app for the time being.
    # should "work when the input is a StringIO" do
    #   rack_input = StringIO.new(@file.read)
    #   upload('rack.input' => rack_input)
    #   assert_file_uploaded
    # end

    should "be forced to perform a file upload if `X-File-Upload: true`" do
      upload('CONTENT_TYPE' => 'multipart/form-data', 'HTTP_X_FILE_UPLOAD' => 'true')
      assert_file_uploaded_as 'multipart/form-data'
    end

    should "not perform a file upload if `X-File-Upload: false`" do
      upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'false')
      assert_successful_non_upload
    end

    should "leave rack.input in a state readable by other middlewares" do
      upload
      assert !last_request.env['rack.input'].eof?, "rack.input should be rewind'd"
    end

    context "when garbage collection runs (Ruby 1.9)" do
      context "and the file is received as a Tempfile" do
        should "ensure that the uploaded file remains" do
          tempfile = Tempfile.new('rack-raw-upload-test-')
          tempfile << @file.read
          tempfile.rewind
          upload('rack.input' => tempfile)
          received = last_request.POST["file"]
          GC.start
          assert File.exists?(received[:tempfile].path)
          assert_file_uploaded
        end
      end

      context "and the file is NOT received as a TmpFile" do
        should "ensure that the uploaded file remains" do
          upload
          received = last_request.POST["file"]
          GC.start
          assert File.exists?(received[:tempfile].path)
          assert_file_uploaded
        end
      end
    end

    context "with X-File-Upload: smart" do
      should "perform a file upload if appropriate" do
        post('HTTP_X_FILE_UPLOAD' => 'smart')
        assert_successful_non_upload
      end

      should "not perform a file upload if not appropriate" do
        upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'smart')
        assert_file_uploaded_as 'image/jpeg'
      end
    end

    context "with :explicit => true" do
      setup do
        @middleware_opts = { :explicit => true }
      end

      should "not be triggered by an appropriate Content-Type" do
        upload('CONTENT_TYPE' => 'image/jpeg')
        assert_successful_non_upload
      end

      should "be triggered by `X-File-Upload: true`" do
        upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'true')
        assert_file_uploaded_as 'image/jpeg'
      end

      should "kick in when `X-File-Upload: smart` and the request is an upload" do
        upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'smart')
        assert_file_uploaded_as 'image/jpeg'
      end

      should "stay put when `X-File-Upload: smart` and the request is not an upload" do
        post('HTTP_X_FILE_UPLOAD' => 'smart')
        assert_successful_non_upload
      end
    end

    context "with a given :tmpdir" do
      setup do
        @tmp_path = File.join(Dir::tmpdir, 'rack-raw-upload/some-dir')
        FileUtils.mkdir_p(@tmp_path)
        @middleware_opts = { :tmpdir => @tmp_path }
      end

      should "use it as temporary file store" do
        upload
        assert Dir.entries(@tmp_path).any?{|node| node =~ /raw-upload/ }
      end
    end

    context "with JSON parameters" do
      setup do
        upload('HTTP_X_JSON_PARAMS' => MultiJson.dump({
          :argument => 'value1',
          'argument with spaces' => 'value 2'
        }))
      end

      should "convert these into arguments" do
        assert_equal last_request.POST['argument'], 'value1'
        assert_equal last_request.POST['argument with spaces'], 'value 2'
      end
    end

    context "with query parameters" do
      setup do
        upload('HTTP_X_PARAMS' => "arg=val1&ar+g=val2")
      end

      should "convert these into arguments" do
        assert_equal last_request.POST['arg'], 'val1'
        assert_equal last_request.POST['ar g'], 'val2'
      end
    end

    context "with query parameters, deprecated style" do
      setup do
        json_params = MultiJson.dump({
          :argument => 'value1',
          'argument with spaces' => 'value 2'
        })
        @mock_error_stream = obj = Object.new
        stub(obj).puts
        stub(obj).flush

        upload({
          'HTTP_X_QUERY_PARAMS' => json_params,
          'rack.errors' => @mock_error_stream
        })
      end

      should "convert these into arguments" do
        assert_equal last_request.POST['argument'], 'value1'
        assert_equal last_request.POST['argument with spaces'], 'value 2'
      end

      should "give a deprecation warning" do
        assert_received(@mock_error_stream) {|subject| subject.puts(is_a(String)) }
      end
    end

    context "with filename" do
      setup do
        upload('HTTP_X_FILE_NAME' => @filename)
      end

      should "be transformed into a normal form upload" do
        assert_equal @filename, last_request.POST["file"][:filename]
      end
    end
  end

  context "path matcher" do
    should "accept any path by default" do
      rru = Rack::RawUpload.new(nil)
      assert rru.upload_path?('/')
      assert rru.upload_path?('/resources.json')
      assert rru.upload_path?('/resources/stuff.json')
    end

    should "accept literal paths" do
      rru = Rack::RawUpload.new nil, :paths => '/resources.json'
      assert rru.upload_path?('/resources.json')
      assert ! rru.upload_path?('/resources.html')
    end

    should "accept paths with wildcards" do
      rru = Rack::RawUpload.new nil, :paths => '/resources.*'
      assert rru.upload_path?('/resources.json')
      assert rru.upload_path?('/resources.*')
      assert ! rru.upload_path?('/resource.json')
      assert ! rru.upload_path?('/resourcess.json')
      assert ! rru.upload_path?('/resources.json/blah')
    end

    should "accept several entries" do
      rru = Rack::RawUpload.new nil, :paths => ['/resources.*', '/uploads']
      assert rru.upload_path?('/uploads')
      assert rru.upload_path?('/resources.*')
      assert ! rru.upload_path?('/upload')
    end
  end

  def assert_file_uploaded
    file = File.open(@path)
    assert received = last_request.POST["file"], "No file received"
    assert_files_equal file, received[:tempfile]
    assert last_response.ok?
  end

  def assert_file_uploaded_as(file_type)
    file = File.open(@path)
    received = last_request.POST["file"]
    assert_files_equal file, received[:tempfile]
    assert_equal file_type, received[:type]
    assert last_response.ok?
  end

  def assert_successful_non_upload
    assert ! last_request.POST.has_key?('file')
    assert last_response.ok?
  end

  def assert_files_equal(f1, f2)
    expected = Digest::MD5.hexdigest(IO.read(f1.path))
    actual   = Digest::MD5.hexdigest(IO.read(f2.path))
    assert_equal expected, actual
  end
end
