# Copyright 2016 Google Inc.
#
# Licensed 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.

import unittest


class Test__Transfer(unittest.TestCase):
    URL = 'http://example.com/api'

    @staticmethod
    def _get_target_class():
        from google.cloud.streaming.transfer import _Transfer
        return _Transfer

    def _make_one(self, *args, **kw):
        return self._get_target_class()(*args, **kw)

    def test_ctor_defaults(self):
        from google.cloud.streaming.transfer import _DEFAULT_CHUNKSIZE
        stream = _Stream()
        xfer = self._make_one(stream)
        self.assertIs(xfer.stream, stream)
        self.assertFalse(xfer.close_stream)
        self.assertEqual(xfer.chunksize, _DEFAULT_CHUNKSIZE)
        self.assertTrue(xfer.auto_transfer)
        self.assertIsNone(xfer.bytes_http)
        self.assertIsNone(xfer.http)
        self.assertEqual(xfer.num_retries, 5)
        self.assertIsNone(xfer.url)
        self.assertFalse(xfer.initialized)

    def test_ctor_explicit(self):
        stream = _Stream()
        HTTP = object()
        CHUNK_SIZE = 1 << 18
        NUM_RETRIES = 8
        xfer = self._make_one(stream,
                              close_stream=True,
                              chunksize=CHUNK_SIZE,
                              auto_transfer=False,
                              http=HTTP,
                              num_retries=NUM_RETRIES)
        self.assertIs(xfer.stream, stream)
        self.assertTrue(xfer.close_stream)
        self.assertEqual(xfer.chunksize, CHUNK_SIZE)
        self.assertFalse(xfer.auto_transfer)
        self.assertIs(xfer.bytes_http, HTTP)
        self.assertIs(xfer.http, HTTP)
        self.assertEqual(xfer.num_retries, NUM_RETRIES)

    def test_bytes_http_fallback_to_http(self):
        stream = _Stream()
        HTTP = object()
        xfer = self._make_one(stream, http=HTTP)
        self.assertIs(xfer.bytes_http, HTTP)

    def test_bytes_http_setter(self):
        stream = _Stream()
        HTTP = object()
        BYTES_HTTP = object()
        xfer = self._make_one(stream, http=HTTP)
        xfer.bytes_http = BYTES_HTTP
        self.assertIs(xfer.bytes_http, BYTES_HTTP)

    def test_num_retries_setter_invalid(self):
        stream = _Stream()
        xfer = self._make_one(stream)
        with self.assertRaises(ValueError):
            xfer.num_retries = object()

    def test_num_retries_setter_negative(self):
        stream = _Stream()
        xfer = self._make_one(stream)
        with self.assertRaises(ValueError):
            xfer.num_retries = -1

    def test__initialize_not_already_initialized_w_http(self):
        HTTP = object()
        stream = _Stream()
        xfer = self._make_one(stream)
        xfer._initialize(HTTP, self.URL)
        self.assertTrue(xfer.initialized)
        self.assertIs(xfer.http, HTTP)
        self.assertIs(xfer.url, self.URL)

    def test__initialize_not_already_initialized_wo_http(self):
        from httplib2 import Http
        stream = _Stream()
        xfer = self._make_one(stream)
        xfer._initialize(None, self.URL)
        self.assertTrue(xfer.initialized)
        self.assertIsInstance(xfer.http, Http)
        self.assertIs(xfer.url, self.URL)

    def test__initialize_w_existing_http(self):
        HTTP_1, HTTP_2 = object(), object()
        stream = _Stream()
        xfer = self._make_one(stream, http=HTTP_1)
        xfer._initialize(HTTP_2, self.URL)
        self.assertTrue(xfer.initialized)
        self.assertIs(xfer.http, HTTP_1)
        self.assertIs(xfer.url, self.URL)

    def test__initialize_already_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        URL_2 = 'http://example.com/other'
        HTTP_1, HTTP_2 = object(), object()
        stream = _Stream()
        xfer = self._make_one(stream)
        xfer._initialize(HTTP_1, self.URL)
        with self.assertRaises(TransferInvalidError):
            xfer._initialize(HTTP_2, URL_2)

    def test__ensure_initialized_hit(self):
        HTTP = object()
        stream = _Stream()
        xfer = self._make_one(stream)
        xfer._initialize(HTTP, self.URL)
        xfer._ensure_initialized()  # no raise

    def test__ensure_initialized_miss(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        stream = _Stream()
        xfer = self._make_one(stream)
        with self.assertRaises(TransferInvalidError):
            xfer._ensure_initialized()

    def test__ensure_uninitialized_hit(self):
        stream = _Stream()
        xfer = self._make_one(stream)
        xfer._ensure_uninitialized()  # no raise

    def test__ensure_uninitialized_miss(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        stream = _Stream()
        HTTP = object()
        xfer = self._make_one(stream)
        xfer._initialize(HTTP, self.URL)
        with self.assertRaises(TransferInvalidError):
            xfer._ensure_uninitialized()

    def test___del___closes_stream(self):

        stream = _Stream()
        xfer = self._make_one(stream, close_stream=True)

        self.assertFalse(stream._closed)
        del xfer
        self.assertTrue(stream._closed)


class Test_Download(unittest.TestCase):
    URL = "http://example.com/api"

    @staticmethod
    def _get_target_class():
        from google.cloud.streaming.transfer import Download
        return Download

    def _make_one(self, *args, **kw):
        return self._get_target_class()(*args, **kw)

    def test_ctor_defaults(self):
        stream = _Stream()
        download = self._make_one(stream)
        self.assertIs(download.stream, stream)
        self.assertIsNone(download._initial_response)
        self.assertEqual(download.progress, 0)
        self.assertIsNone(download.total_size)
        self.assertIsNone(download.encoding)

    def test_ctor_w_kwds(self):
        stream = _Stream()
        CHUNK_SIZE = 123
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        self.assertIs(download.stream, stream)
        self.assertEqual(download.chunksize, CHUNK_SIZE)

    def test_ctor_w_total_size(self):
        stream = _Stream()
        SIZE = 123
        download = self._make_one(stream, total_size=SIZE)
        self.assertIs(download.stream, stream)
        self.assertEqual(download.total_size, SIZE)

    def test_from_file_w_existing_file_no_override(self):
        import os
        from google.cloud._testing import _tempdir
        klass = self._get_target_class()
        with _tempdir() as tempdir:
            filename = os.path.join(tempdir, 'file.out')
            with open(filename, 'w') as fileobj:
                fileobj.write('EXISTING FILE')
            with self.assertRaises(ValueError):
                klass.from_file(filename)

    def test_from_file_w_existing_file_w_override_wo_auto_transfer(self):
        import os
        from google.cloud._testing import _tempdir
        klass = self._get_target_class()
        with _tempdir() as tempdir:
            filename = os.path.join(tempdir, 'file.out')
            with open(filename, 'w') as fileobj:
                fileobj.write('EXISTING FILE')
            download = klass.from_file(filename, overwrite=True,
                                       auto_transfer=False)
            self.assertFalse(download.auto_transfer)
            del download  # closes stream
            with open(filename, 'rb') as fileobj:
                self.assertEqual(fileobj.read(), b'')

    def test_from_stream_defaults(self):
        stream = _Stream()
        klass = self._get_target_class()
        download = klass.from_stream(stream)
        self.assertIs(download.stream, stream)
        self.assertTrue(download.auto_transfer)
        self.assertIsNone(download.total_size)

    def test_from_stream_explicit(self):
        CHUNK_SIZE = 1 << 18
        SIZE = 123
        stream = _Stream()
        klass = self._get_target_class()
        download = klass.from_stream(stream, auto_transfer=False,
                                     total_size=SIZE, chunksize=CHUNK_SIZE)
        self.assertIs(download.stream, stream)
        self.assertFalse(download.auto_transfer)
        self.assertEqual(download.total_size, SIZE)
        self.assertEqual(download.chunksize, CHUNK_SIZE)

    def test_configure_request(self):
        CHUNK_SIZE = 100
        download = self._make_one(_Stream(), chunksize=CHUNK_SIZE)
        request = _Dummy(headers={})
        url_builder = _Dummy(query_params={})
        download.configure_request(request, url_builder)
        self.assertEqual(request.headers, {'Range': 'bytes=0-99'})
        self.assertEqual(url_builder.query_params, {'alt': 'media'})

    def test__set_total_wo_content_range_wo_existing_total(self):
        info = {}
        download = self._make_one(_Stream())
        download._set_total(info)
        self.assertEqual(download.total_size, 0)

    def test__set_total_wo_content_range_w_existing_total(self):
        SIZE = 123
        info = {}
        download = self._make_one(_Stream(), total_size=SIZE)
        download._set_total(info)
        self.assertEqual(download.total_size, SIZE)

    def test__set_total_w_content_range_w_existing_total(self):
        SIZE = 123
        info = {'content-range': 'bytes 123-234/4567'}
        download = self._make_one(_Stream(), total_size=SIZE)
        download._set_total(info)
        self.assertEqual(download.total_size, 4567)

    def test__set_total_w_content_range_w_asterisk_total(self):
        info = {'content-range': 'bytes 123-234/*'}
        download = self._make_one(_Stream())
        download._set_total(info)
        self.assertEqual(download.total_size, 0)

    def test_initialize_download_already_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        request = _Request()
        download = self._make_one(_Stream())
        download._initialize(None, self.URL)
        with self.assertRaises(TransferInvalidError):
            download.initialize_download(request, http=object())

    def test_initialize_download_wo_autotransfer(self):
        request = _Request()
        http = object()
        download = self._make_one(_Stream(), auto_transfer=False)
        download.initialize_download(request, http)
        self.assertIs(download.http, http)
        self.assertEqual(download.url, request.url)

    def test_initialize_download_w_autotransfer_failing(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import HttpError
        request = _Request()
        http = object()
        download = self._make_one(_Stream(), auto_transfer=True)

        response = _makeResponse(http_client.BAD_REQUEST)
        requester = _MakeRequest(response)

        with _Monkey(MUT, make_api_request=requester):
            with self.assertRaises(HttpError):
                download.initialize_download(request, http)

        self.assertTrue(len(requester._requested), 1)
        self.assertIs(requester._requested[0][0], request)

    def test_initialize_download_w_autotransfer_w_content_location(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        REDIRECT_URL = 'http://example.com/other'
        request = _Request()
        http = object()
        info = {'content-location': REDIRECT_URL}
        download = self._make_one(_Stream(), auto_transfer=True)

        response = _makeResponse(http_client.NO_CONTENT, info)
        requester = _MakeRequest(response)

        with _Monkey(MUT, make_api_request=requester):
            download.initialize_download(request, http)

        self.assertIsNone(download._initial_response)
        self.assertEqual(download.total_size, 0)
        self.assertIs(download.http, http)
        self.assertEqual(download.url, REDIRECT_URL)
        self.assertTrue(len(requester._requested), 1)
        self.assertIs(requester._requested[0][0], request)

    def test__normalize_start_end_w_end_w_start_lt_0(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())

        with self.assertRaises(TransferInvalidError):
            download._normalize_start_end(-1, 0)

    def test__normalize_start_end_w_end_w_start_gt_total(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())
        download._set_total({'content-range': 'bytes 0-1/2'})

        with self.assertRaises(TransferInvalidError):
            download._normalize_start_end(3, 0)

    def test__normalize_start_end_w_end_lt_start(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())
        download._set_total({'content-range': 'bytes 0-1/2'})

        with self.assertRaises(TransferInvalidError):
            download._normalize_start_end(1, 0)

    def test__normalize_start_end_w_end_gt_start(self):
        download = self._make_one(_Stream())
        download._set_total({'content-range': 'bytes 0-1/2'})
        self.assertEqual(download._normalize_start_end(1, 2), (1, 1))

    def test__normalize_start_end_wo_end_w_start_lt_0(self):
        download = self._make_one(_Stream())
        download._set_total({'content-range': 'bytes 0-1/2'})
        self.assertEqual(download._normalize_start_end(-2), (0, 1))
        self.assertEqual(download._normalize_start_end(-1), (1, 1))

    def test__normalize_start_end_wo_end_w_start_ge_0(self):
        download = self._make_one(_Stream())
        download._set_total({'content-range': 'bytes 0-1/100'})
        self.assertEqual(download._normalize_start_end(0), (0, 99))
        self.assertEqual(download._normalize_start_end(1), (1, 99))

    def test__set_range_header_w_start_lt_0(self):
        request = _Request()
        download = self._make_one(_Stream())
        download._set_range_header(request, -1)
        self.assertEqual(request.headers['range'], 'bytes=-1')

    def test__set_range_header_w_start_ge_0_wo_end(self):
        request = _Request()
        download = self._make_one(_Stream())
        download._set_range_header(request, 0)
        self.assertEqual(request.headers['range'], 'bytes=0-')

    def test__set_range_header_w_start_ge_0_w_end(self):
        request = _Request()
        download = self._make_one(_Stream())
        download._set_range_header(request, 0, 1)
        self.assertEqual(request.headers['range'], 'bytes=0-1')

    def test__compute_end_byte_w_start_lt_0_w_end(self):
        download = self._make_one(_Stream())
        self.assertEqual(download._compute_end_byte(-1, 1), 1)

    def test__compute_end_byte_w_start_ge_0_wo_end_w_use_chunks(self):
        CHUNK_SIZE = 5
        download = self._make_one(_Stream(), chunksize=CHUNK_SIZE)
        self.assertEqual(download._compute_end_byte(0, use_chunks=True), 4)

    def test__compute_end_byte_w_start_ge_0_w_end_w_use_chunks(self):
        CHUNK_SIZE = 5
        download = self._make_one(_Stream(), chunksize=CHUNK_SIZE)
        self.assertEqual(download._compute_end_byte(0, 3, use_chunks=True), 3)
        self.assertEqual(download._compute_end_byte(0, 5, use_chunks=True), 4)

    def test__compute_end_byte_w_start_ge_0_w_end_w_total_size(self):
        CHUNK_SIZE = 50
        download = self._make_one(_Stream(), chunksize=CHUNK_SIZE)
        download._set_total({'content-range': 'bytes 0-1/10'})
        self.assertEqual(download._compute_end_byte(0, 100, use_chunks=False),
                         9)
        self.assertEqual(download._compute_end_byte(0, 8, use_chunks=False), 8)

    def test__compute_end_byte_w_start_ge_0_wo_end_w_total_size(self):
        CHUNK_SIZE = 50
        download = self._make_one(_Stream(), chunksize=CHUNK_SIZE)
        download._set_total({'content-range': 'bytes 0-1/10'})
        self.assertEqual(download._compute_end_byte(0, use_chunks=False), 9)

    def test__get_chunk_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())

        with self.assertRaises(TransferInvalidError):
            download._get_chunk(0, 10)

    def test__get_chunk(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        http = object()
        download = self._make_one(_Stream())
        download._initialize(http, self.URL)
        response = _makeResponse(http_client.OK)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            found = download._get_chunk(0, 10)

        self.assertIs(found, response)
        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers['range'], 'bytes=0-10')

    def test__process_response_w_FORBIDDEN(self):
        from google.cloud.streaming.exceptions import HttpError
        from six.moves import http_client
        download = self._make_one(_Stream())
        response = _makeResponse(http_client.FORBIDDEN)
        with self.assertRaises(HttpError):
            download._process_response(response)

    def test__process_response_w_NOT_FOUND(self):
        from google.cloud.streaming.exceptions import HttpError
        from six.moves import http_client
        download = self._make_one(_Stream())
        response = _makeResponse(http_client.NOT_FOUND)
        with self.assertRaises(HttpError):
            download._process_response(response)

    def test__process_response_w_other_error(self):
        from google.cloud.streaming.exceptions import TransferRetryError
        from six.moves import http_client
        download = self._make_one(_Stream())
        response = _makeResponse(http_client.BAD_REQUEST)
        with self.assertRaises(TransferRetryError):
            download._process_response(response)

    def test__process_response_w_OK_wo_encoding(self):
        from six.moves import http_client
        stream = _Stream()
        download = self._make_one(stream)
        response = _makeResponse(http_client.OK, content='OK')
        found = download._process_response(response)
        self.assertIs(found, response)
        self.assertEqual(stream._written, ['OK'])
        self.assertEqual(download.progress, 2)
        self.assertIsNone(download.encoding)

    def test__process_response_w_PARTIAL_CONTENT_w_encoding(self):
        from six.moves import http_client
        stream = _Stream()
        download = self._make_one(stream)
        info = {'content-encoding': 'blah'}
        response = _makeResponse(http_client.OK, info, 'PARTIAL')
        found = download._process_response(response)
        self.assertIs(found, response)
        self.assertEqual(stream._written, ['PARTIAL'])
        self.assertEqual(download.progress, 7)
        self.assertEqual(download.encoding, 'blah')

    def test__process_response_w_REQUESTED_RANGE_NOT_SATISFIABLE(self):
        from six.moves import http_client
        stream = _Stream()
        download = self._make_one(stream)
        response = _makeResponse(
            http_client.REQUESTED_RANGE_NOT_SATISFIABLE)
        found = download._process_response(response)
        self.assertIs(found, response)
        self.assertEqual(stream._written, [])
        self.assertEqual(download.progress, 0)
        self.assertIsNone(download.encoding)

    def test__process_response_w_NO_CONTENT(self):
        from six.moves import http_client
        stream = _Stream()
        download = self._make_one(stream)
        response = _makeResponse(status_code=http_client.NO_CONTENT)
        found = download._process_response(response)
        self.assertIs(found, response)
        self.assertEqual(stream._written, [''])
        self.assertEqual(download.progress, 0)
        self.assertIsNone(download.encoding)

    def test_get_range_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())
        with self.assertRaises(TransferInvalidError):
            download.get_range(0, 10)

    def test_get_range_wo_total_size_complete(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        REQ_RANGE = 'bytes=0-%d' % (LEN,)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN)
        http = object()
        stream = _Stream()
        download = self._make_one(stream)
        download._initialize(http, self.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.get_range(0, LEN)

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [CONTENT])
        self.assertEqual(download.total_size, LEN)

    def test_get_range_wo_total_size_wo_end(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        START = 5
        CHUNK_SIZE = 123
        REQ_RANGE = 'bytes=%d-%d' % (START, START + CHUNK_SIZE - 1,)
        RESP_RANGE = 'bytes %d-%d/%d' % (START, LEN - 1, LEN)
        http = object()
        stream = _Stream()
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        download._initialize(http, self.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT[START:])
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.get_range(START)

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [CONTENT[START:]])
        self.assertEqual(download.total_size, LEN)

    def test_get_range_w_total_size_partial(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        PARTIAL_LEN = 5
        REQ_RANGE = 'bytes=0-%d' % (PARTIAL_LEN,)
        RESP_RANGE = 'bytes 0-%d/%d' % (PARTIAL_LEN, LEN,)
        http = object()
        stream = _Stream()
        download = self._make_one(stream, total_size=LEN)
        download._initialize(http, self.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT[:PARTIAL_LEN])
        response.length = LEN
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.get_range(0, PARTIAL_LEN)

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [CONTENT[:PARTIAL_LEN]])
        self.assertEqual(download.total_size, LEN)

    def test_get_range_w_empty_chunk(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import TransferRetryError
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        START = 5
        CHUNK_SIZE = 123
        REQ_RANGE = 'bytes=%d-%d' % (START, START + CHUNK_SIZE - 1,)
        RESP_RANGE = 'bytes %d-%d/%d' % (START, LEN - 1, LEN)
        http = object()
        stream = _Stream()
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        download._initialize(http, self.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            with self.assertRaises(TransferRetryError):
                download.get_range(START)

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [''])
        self.assertEqual(download.total_size, LEN)

    def test_get_range_w_total_size_wo_use_chunks(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        CHUNK_SIZE = 3
        REQ_RANGE = 'bytes=0-%d' % (LEN - 1,)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN,)
        http = object()
        stream = _Stream()
        download = self._make_one(stream, total_size=LEN, chunksize=CHUNK_SIZE)
        download._initialize(http, self.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.get_range(0, use_chunks=False)

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [CONTENT])
        self.assertEqual(download.total_size, LEN)

    def test_get_range_w_multiple_chunks(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDE'
        LEN = len(CONTENT)
        CHUNK_SIZE = 3
        REQ_RANGE_1 = 'bytes=0-%d' % (CHUNK_SIZE - 1,)
        RESP_RANGE_1 = 'bytes 0-%d/%d' % (CHUNK_SIZE - 1, LEN)
        REQ_RANGE_2 = 'bytes=%d-%d' % (CHUNK_SIZE, LEN - 1)
        RESP_RANGE_2 = 'bytes %d-%d/%d' % (CHUNK_SIZE, LEN - 1, LEN)
        http = object()
        stream = _Stream()
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        download._initialize(http, self.URL)
        info_1 = {'content-range': RESP_RANGE_1}
        response_1 = _makeResponse(http_client.PARTIAL_CONTENT, info_1,
                                   CONTENT[:CHUNK_SIZE])
        info_2 = {'content-range': RESP_RANGE_2}
        response_2 = _makeResponse(http_client.OK, info_2,
                                   CONTENT[CHUNK_SIZE:])
        requester = _MakeRequest(response_1, response_2)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.get_range(0)

        self.assertTrue(len(requester._requested), 2)
        request_1 = requester._requested[0][0]
        self.assertEqual(request_1.headers, {'range': REQ_RANGE_1})
        request_2 = requester._requested[1][0]
        self.assertEqual(request_2.headers, {'range': REQ_RANGE_2})
        self.assertEqual(stream._written, [b'ABC', b'DE'])
        self.assertEqual(download.total_size, LEN)

    def test_stream_file_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        download = self._make_one(_Stream())

        with self.assertRaises(TransferInvalidError):
            download.stream_file()

    def test_stream_file_w_initial_response_complete(self):
        from six.moves import http_client
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN,)
        stream = _Stream()
        download = self._make_one(stream, total_size=LEN)
        info = {'content-range': RESP_RANGE}
        download._initial_response = _makeResponse(
            http_client.OK, info, CONTENT)
        http = object()
        download._initialize(http, _Request.URL)

        download.stream_file()

        self.assertEqual(stream._written, [CONTENT])
        self.assertEqual(download.total_size, LEN)

    def test_stream_file_w_initial_response_incomplete(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CHUNK_SIZE = 3
        CONTENT = b'ABCDEF'
        LEN = len(CONTENT)
        RESP_RANGE_1 = 'bytes 0-%d/%d' % (CHUNK_SIZE - 1, LEN,)
        REQ_RANGE_2 = 'bytes=%d-%d' % (CHUNK_SIZE, LEN - 1)
        RESP_RANGE_2 = 'bytes %d-%d/%d' % (CHUNK_SIZE, LEN - 1, LEN,)
        stream = _Stream()
        http = object()
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        info_1 = {'content-range': RESP_RANGE_1}
        download._initial_response = _makeResponse(
            http_client.PARTIAL_CONTENT, info_1, CONTENT[:CHUNK_SIZE])
        info_2 = {'content-range': RESP_RANGE_2}
        response_2 = _makeResponse(
            http_client.OK, info_2, CONTENT[CHUNK_SIZE:])
        requester = _MakeRequest(response_2)

        download._initialize(http, _Request.URL)

        request = _Request()

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.stream_file()

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE_2})
        self.assertEqual(stream._written,
                         [CONTENT[:CHUNK_SIZE], CONTENT[CHUNK_SIZE:]])
        self.assertEqual(download.total_size, LEN)

    def test_stream_file_wo_initial_response_wo_total_size(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        CHUNK_SIZE = 123
        REQ_RANGE = 'bytes=0-%d' % (CHUNK_SIZE - 1)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN,)
        stream = _Stream()
        http = object()
        download = self._make_one(stream, chunksize=CHUNK_SIZE)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT)
        requester = _MakeRequest(response)
        download._initialize(http, _Request.URL)

        request = _Request()

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            download.stream_file()

        self.assertTrue(len(requester._requested), 1)
        request = requester._requested[0][0]
        self.assertEqual(request.headers, {'range': REQ_RANGE})
        self.assertEqual(stream._written, [CONTENT])
        self.assertEqual(download.total_size, LEN)


class Test_Upload(unittest.TestCase):
    URL = "http://example.com/api"
    MIME_TYPE = 'application/octet-stream'
    UPLOAD_URL = 'http://example.com/upload/id=foobar'

    @staticmethod
    def _get_target_class():
        from google.cloud.streaming.transfer import Upload
        return Upload

    def _make_one(self, stream, mime_type=MIME_TYPE, *args, **kw):
        return self._get_target_class()(stream, mime_type, *args, **kw)

    def test_ctor_defaults(self):
        from google.cloud.streaming.transfer import _DEFAULT_CHUNKSIZE
        stream = _Stream()
        upload = self._make_one(stream)
        self.assertIs(upload.stream, stream)
        self.assertIsNone(upload._final_response)
        self.assertIsNone(upload._server_chunk_granularity)
        self.assertFalse(upload.complete)
        self.assertEqual(upload.mime_type, self.MIME_TYPE)
        self.assertEqual(upload.progress, 0)
        self.assertIsNone(upload.strategy)
        self.assertIsNone(upload.total_size)
        self.assertEqual(upload.chunksize, _DEFAULT_CHUNKSIZE)

    def test_ctor_w_kwds(self):
        stream = _Stream()
        CHUNK_SIZE = 123
        upload = self._make_one(stream, chunksize=CHUNK_SIZE)
        self.assertIs(upload.stream, stream)
        self.assertEqual(upload.mime_type, self.MIME_TYPE)
        self.assertEqual(upload.chunksize, CHUNK_SIZE)

    def test_from_file_w_nonesuch_file(self):
        klass = self._get_target_class()
        filename = '~nosuchuser/file.txt'
        with self.assertRaises(OSError):
            klass.from_file(filename)

    def test_from_file_wo_mimetype_w_unguessable_filename(self):
        import os
        from google.cloud._testing import _tempdir
        klass = self._get_target_class()
        CONTENT = b'EXISTING FILE W/ UNGUESSABLE MIMETYPE'
        with _tempdir() as tempdir:
            filename = os.path.join(tempdir, 'file.unguessable')
            with open(filename, 'wb') as fileobj:
                fileobj.write(CONTENT)
            with self.assertRaises(ValueError):
                klass.from_file(filename)

    def test_from_file_wo_mimetype_w_guessable_filename(self):
        import os
        from google.cloud._testing import _tempdir
        klass = self._get_target_class()
        CONTENT = b'EXISTING FILE W/ GUESSABLE MIMETYPE'
        with _tempdir() as tempdir:
            filename = os.path.join(tempdir, 'file.txt')
            with open(filename, 'wb') as fileobj:
                fileobj.write(CONTENT)
            upload = klass.from_file(filename)
            self.assertEqual(upload.mime_type, 'text/plain')
            self.assertTrue(upload.auto_transfer)
            self.assertEqual(upload.total_size, len(CONTENT))
            upload._stream.close()

    def test_from_file_w_mimetype_w_auto_transfer_w_kwds(self):
        import os
        from google.cloud._testing import _tempdir
        klass = self._get_target_class()
        CONTENT = b'EXISTING FILE W/ GUESSABLE MIMETYPE'
        CHUNK_SIZE = 3
        with _tempdir() as tempdir:
            filename = os.path.join(tempdir, 'file.unguessable')
            with open(filename, 'wb') as fileobj:
                fileobj.write(CONTENT)
            upload = klass.from_file(
                filename,
                mime_type=self.MIME_TYPE,
                auto_transfer=False,
                chunksize=CHUNK_SIZE)
            self.assertEqual(upload.mime_type, self.MIME_TYPE)
            self.assertFalse(upload.auto_transfer)
            self.assertEqual(upload.total_size, len(CONTENT))
            self.assertEqual(upload.chunksize, CHUNK_SIZE)
            upload._stream.close()

    def test_from_stream_wo_mimetype(self):
        klass = self._get_target_class()
        stream = _Stream()
        with self.assertRaises(ValueError):
            klass.from_stream(stream, mime_type=None)

    def test_from_stream_defaults(self):
        klass = self._get_target_class()
        stream = _Stream()
        upload = klass.from_stream(stream, mime_type=self.MIME_TYPE)
        self.assertEqual(upload.mime_type, self.MIME_TYPE)
        self.assertTrue(upload.auto_transfer)
        self.assertIsNone(upload.total_size)

    def test_from_stream_explicit(self):
        klass = self._get_target_class()
        stream = _Stream()
        SIZE = 10
        CHUNK_SIZE = 3
        upload = klass.from_stream(
            stream,
            mime_type=self.MIME_TYPE,
            auto_transfer=False,
            total_size=SIZE,
            chunksize=CHUNK_SIZE)
        self.assertEqual(upload.mime_type, self.MIME_TYPE)
        self.assertFalse(upload.auto_transfer)
        self.assertEqual(upload.total_size, SIZE)
        self.assertEqual(upload.chunksize, CHUNK_SIZE)

    def test_strategy_setter_invalid(self):
        upload = self._make_one(_Stream())
        with self.assertRaises(ValueError):
            upload.strategy = object()
        with self.assertRaises(ValueError):
            upload.strategy = 'unknown'

    def test_strategy_setter_SIMPLE_UPLOAD(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        upload = self._make_one(_Stream())
        upload.strategy = SIMPLE_UPLOAD
        self.assertEqual(upload.strategy, SIMPLE_UPLOAD)

    def test_strategy_setter_RESUMABLE_UPLOAD(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        upload = self._make_one(_Stream())
        upload.strategy = RESUMABLE_UPLOAD
        self.assertEqual(upload.strategy, RESUMABLE_UPLOAD)

    def test_total_size_setter_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        SIZE = 123
        upload = self._make_one(_Stream)
        http = object()
        upload._initialize(http, _Request.URL)
        with self.assertRaises(TransferInvalidError):
            upload.total_size = SIZE

    def test_total_size_setter_not_initialized(self):
        SIZE = 123
        upload = self._make_one(_Stream)
        upload.total_size = SIZE
        self.assertEqual(upload.total_size, SIZE)

    def test__set_default_strategy_w_existing_strategy(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        config = _Dummy(
            resumable_path='/resumable/endpoint',
            simple_multipart=True,
            simple_path='/upload/endpoint',
        )
        request = _Request()
        upload = self._make_one(_Stream)
        upload.strategy = RESUMABLE_UPLOAD
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, RESUMABLE_UPLOAD)

    def test__set_default_strategy_wo_resumable_path(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        config = _Dummy(
            resumable_path=None,
            simple_multipart=True,
            simple_path='/upload/endpoint',
        )
        request = _Request()
        upload = self._make_one(_Stream())
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, SIMPLE_UPLOAD)

    def test__set_default_strategy_w_total_size_gt_threshhold(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD_THRESHOLD
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        config = _UploadConfig()
        request = _Request()
        upload = self._make_one(
            _Stream(), total_size=RESUMABLE_UPLOAD_THRESHOLD + 1)
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, RESUMABLE_UPLOAD)

    def test__set_default_strategy_w_body_wo_multipart(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        config = _UploadConfig()
        config.simple_multipart = False
        request = _Request(body=CONTENT)
        upload = self._make_one(_Stream(), total_size=len(CONTENT))
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, RESUMABLE_UPLOAD)

    def test__set_default_strategy_w_body_w_multipart_wo_simple_path(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        config = _UploadConfig()
        config.simple_path = None
        request = _Request(body=CONTENT)
        upload = self._make_one(_Stream(), total_size=len(CONTENT))
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, RESUMABLE_UPLOAD)

    def test__set_default_strategy_w_body_w_multipart_w_simple_path(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        config = _UploadConfig()
        request = _Request(body=CONTENT)
        upload = self._make_one(_Stream(), total_size=len(CONTENT))
        upload._set_default_strategy(config, request)
        self.assertEqual(upload.strategy, SIMPLE_UPLOAD)

    def test_configure_request_w_total_size_gt_max_size(self):
        MAX_SIZE = 1000
        config = _UploadConfig()
        config.max_size = MAX_SIZE
        request = _Request()
        url_builder = _Dummy()
        upload = self._make_one(_Stream(), total_size=MAX_SIZE + 1)
        with self.assertRaises(ValueError):
            upload.configure_request(config, request, url_builder)

    def test_configure_request_w_invalid_mimetype(self):
        config = _UploadConfig()
        config.accept = ('text/*',)
        request = _Request()
        url_builder = _Dummy()
        upload = self._make_one(_Stream())
        with self.assertRaises(ValueError):
            upload.configure_request(config, request, url_builder)

    def test_configure_request_w_simple_wo_body(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        CONTENT = b'CONTENT'
        config = _UploadConfig()
        request = _Request()
        url_builder = _Dummy(query_params={})
        upload = self._make_one(_Stream(CONTENT))
        upload.strategy = SIMPLE_UPLOAD

        upload.configure_request(config, request, url_builder)

        self.assertEqual(url_builder.query_params, {'uploadType': 'media'})
        self.assertEqual(url_builder.relative_path, config.simple_path)

        self.assertEqual(request.headers, {'content-type': self.MIME_TYPE})
        self.assertEqual(request.body, CONTENT)
        self.assertEqual(request.loggable_body, '<media body>')

    def test_configure_request_w_simple_w_body(self):
        from google.cloud._helpers import _to_bytes
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        CONTENT = b'CONTENT'
        BODY = b'BODY'
        config = _UploadConfig()
        request = _Request(body=BODY)
        request.headers['content-type'] = 'text/plain'
        url_builder = _Dummy(query_params={})
        upload = self._make_one(_Stream(CONTENT))
        upload.strategy = SIMPLE_UPLOAD

        upload.configure_request(config, request, url_builder)

        self.assertEqual(url_builder.query_params, {'uploadType': 'multipart'})
        self.assertEqual(url_builder.relative_path, config.simple_path)

        self.assertEqual(list(request.headers), ['content-type'])
        ctype, boundary = [x.strip()
                           for x in request.headers['content-type'].split(';')]
        self.assertEqual(ctype, 'multipart/related')
        self.assertTrue(boundary.startswith('boundary="=='))
        self.assertTrue(boundary.endswith('=="'))

        divider = b'--' + _to_bytes(boundary[len('boundary="'):-1])
        chunks = request.body.split(divider)[1:-1]  # discard prolog / epilog
        self.assertEqual(len(chunks), 2)

        parse_chunk = _email_chunk_parser()
        text_msg = parse_chunk(chunks[0].strip())
        self.assertEqual(dict(text_msg._headers),
                         {'Content-Type': 'text/plain',
                          'MIME-Version': '1.0'})
        self.assertEqual(text_msg._payload, BODY.decode('ascii'))

        app_msg = parse_chunk(chunks[1].strip())
        self.assertEqual(dict(app_msg._headers),
                         {'Content-Type': self.MIME_TYPE,
                          'Content-Transfer-Encoding': 'binary',
                          'MIME-Version': '1.0'})
        self.assertEqual(app_msg._payload, CONTENT.decode('ascii'))
        self.assertTrue(b'<media body>' in request.loggable_body)

    def test_configure_request_w_resumable_wo_total_size(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'CONTENT'
        config = _UploadConfig()
        request = _Request()
        url_builder = _Dummy(query_params={})
        upload = self._make_one(_Stream(CONTENT))
        upload.strategy = RESUMABLE_UPLOAD

        upload.configure_request(config, request, url_builder)

        self.assertEqual(url_builder.query_params, {'uploadType': 'resumable'})
        self.assertEqual(url_builder.relative_path, config.resumable_path)

        self.assertEqual(request.headers,
                         {'X-Upload-Content-Type': self.MIME_TYPE})

    def test_configure_request_w_resumable_w_total_size(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'CONTENT'
        LEN = len(CONTENT)
        config = _UploadConfig()
        request = _Request()
        url_builder = _Dummy(query_params={})
        upload = self._make_one(_Stream(CONTENT))
        upload.total_size = LEN
        upload.strategy = RESUMABLE_UPLOAD

        upload.configure_request(config, request, url_builder)

        self.assertEqual(url_builder.query_params, {'uploadType': 'resumable'})
        self.assertEqual(url_builder.relative_path, config.resumable_path)

        self.assertEqual(request.headers,
                         {'X-Upload-Content-Type': self.MIME_TYPE,
                          'X-Upload-Content-Length': '%d' % (LEN,)})

    def test_refresh_upload_state_w_simple_strategy(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        upload = self._make_one(_Stream())
        upload.strategy = SIMPLE_UPLOAD
        upload.refresh_upload_state()  # no-op

    def test_refresh_upload_state_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        upload = self._make_one(_Stream())
        upload.strategy = RESUMABLE_UPLOAD
        with self.assertRaises(TransferInvalidError):
            upload.refresh_upload_state()

    def test_refresh_upload_state_w_OK(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN,)
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=LEN)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, _Request.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.OK, info, CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            upload.refresh_upload_state()

        self.assertTrue(upload.complete)
        self.assertEqual(upload.progress, LEN)
        self.assertEqual(stream.tell(), LEN)
        self.assertIs(upload._final_response, response)

    def test_refresh_upload_state_w_CREATED(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        RESP_RANGE = 'bytes 0-%d/%d' % (LEN - 1, LEN,)
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=LEN)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, _Request.URL)
        info = {'content-range': RESP_RANGE}
        response = _makeResponse(http_client.CREATED, info, CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            upload.refresh_upload_state()

        self.assertTrue(upload.complete)
        self.assertEqual(upload.progress, LEN)
        self.assertEqual(stream.tell(), LEN)
        self.assertIs(upload._final_response, response)

    def test_refresh_upload_state_w_RESUME_INCOMPLETE_w_range(self):
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        from google.cloud._testing import _Monkey
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        LAST = 5
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=LEN)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, _Request.URL)
        info = {'range': '0-%d' % (LAST - 1,)}
        response = _makeResponse(RESUME_INCOMPLETE, info, CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            upload.refresh_upload_state()

        self.assertFalse(upload.complete)
        self.assertEqual(upload.progress, LAST)
        self.assertEqual(stream.tell(), LAST)
        self.assertIsNot(upload._final_response, response)

    def test_refresh_upload_state_w_RESUME_INCOMPLETE_wo_range(self):
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        from google.cloud._testing import _Monkey
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=LEN)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, _Request.URL)
        response = _makeResponse(RESUME_INCOMPLETE, content=CONTENT)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            upload.refresh_upload_state()

        self.assertFalse(upload.complete)
        self.assertEqual(upload.progress, 0)
        self.assertEqual(stream.tell(), 0)
        self.assertIsNot(upload._final_response, response)

    def test_refresh_upload_state_w_error(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import HttpError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        LEN = len(CONTENT)
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=LEN)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, _Request.URL)
        response = _makeResponse(http_client.FORBIDDEN)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            with self.assertRaises(HttpError):
                upload.refresh_upload_state()

    def test__get_range_header_miss(self):
        upload = self._make_one(_Stream())
        response = _makeResponse(None)
        self.assertIsNone(upload._get_range_header(response))

    def test__get_range_header_w_Range(self):
        upload = self._make_one(_Stream())
        response = _makeResponse(None, {'Range': '123'})
        self.assertEqual(upload._get_range_header(response), '123')

    def test__get_range_header_w_range(self):
        upload = self._make_one(_Stream())
        response = _makeResponse(None, {'range': '123'})
        self.assertEqual(upload._get_range_header(response), '123')

    def test_initialize_upload_no_strategy(self):
        request = _Request()
        upload = self._make_one(_Stream())
        with self.assertRaises(ValueError):
            upload.initialize_upload(request, http=object())

    def test_initialize_upload_simple_w_http(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        request = _Request()
        upload = self._make_one(_Stream())
        upload.strategy = SIMPLE_UPLOAD
        upload.initialize_upload(request, http=object())  # no-op

    def test_initialize_upload_resumable_already_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        request = _Request()
        upload = self._make_one(_Stream())
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(None, self.URL)
        with self.assertRaises(TransferInvalidError):
            upload.initialize_upload(request, http=object())

    def test_initialize_upload_w_http_resumable_not_initialized_w_error(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import HttpError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        request = _Request()
        upload = self._make_one(_Stream())
        upload.strategy = RESUMABLE_UPLOAD
        response = _makeResponse(http_client.FORBIDDEN)
        requester = _MakeRequest(response)

        with _Monkey(MUT, make_api_request=requester):
            with self.assertRaises(HttpError):
                upload.initialize_upload(request, http=object())

    def test_initialize_upload_w_http_wo_auto_transfer_w_OK(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        request = _Request()
        upload = self._make_one(_Stream(), auto_transfer=False)
        upload.strategy = RESUMABLE_UPLOAD
        info = {'location': self.UPLOAD_URL}
        response = _makeResponse(http_client.OK, info)
        requester = _MakeRequest(response)

        with _Monkey(MUT, make_api_request=requester):
            upload.initialize_upload(request, http=object())

        self.assertIsNone(upload._server_chunk_granularity)
        self.assertEqual(upload.url, self.UPLOAD_URL)
        self.assertEqual(requester._responses, [])
        self.assertEqual(len(requester._requested), 1)
        self.assertIs(requester._requested[0][0], request)

    def test_initialize_upload_w_granularity_w_auto_transfer_w_OK(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        request = _Request()
        upload = self._make_one(_Stream(CONTENT), chunksize=1000)
        upload.strategy = RESUMABLE_UPLOAD
        info = {'X-Goog-Upload-Chunk-Granularity': '100',
                'location': self.UPLOAD_URL}
        response = _makeResponse(http_client.OK, info)
        chunk_response = _makeResponse(http_client.OK)
        requester = _MakeRequest(response, chunk_response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            upload.initialize_upload(request, http)

        self.assertEqual(upload._server_chunk_granularity, 100)
        self.assertEqual(upload.url, self.UPLOAD_URL)
        self.assertEqual(requester._responses, [])
        self.assertEqual(len(requester._requested), 2)
        self.assertIs(requester._requested[0][0], request)
        chunk_request = requester._requested[1][0]
        self.assertIsInstance(chunk_request, _Request)
        self.assertEqual(chunk_request.url, self.UPLOAD_URL)
        self.assertEqual(chunk_request.http_method, 'PUT')
        self.assertEqual(chunk_request.body, CONTENT)

    def test__last_byte(self):
        upload = self._make_one(_Stream())
        self.assertEqual(upload._last_byte('123-456'), 456)

    def test__validate_chunksize_wo__server_chunk_granularity(self):
        upload = self._make_one(_Stream())
        upload._validate_chunksize(123)  # no-op

    def test__validate_chunksize_w__server_chunk_granularity_miss(self):
        upload = self._make_one(_Stream())
        upload._server_chunk_granularity = 100
        with self.assertRaises(ValueError):
            upload._validate_chunksize(123)

    def test__validate_chunksize_w__server_chunk_granularity_hit(self):
        upload = self._make_one(_Stream())
        upload._server_chunk_granularity = 100
        upload._validate_chunksize(400)

    def test_stream_file_w_simple_strategy(self):
        from google.cloud.streaming.transfer import SIMPLE_UPLOAD
        upload = self._make_one(_Stream())
        upload.strategy = SIMPLE_UPLOAD
        with self.assertRaises(ValueError):
            upload.stream_file()

    def test_stream_file_w_use_chunks_invalid_chunk_size(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        upload = self._make_one(_Stream(), chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 100
        with self.assertRaises(ValueError):
            upload.stream_file(use_chunks=True)

    def test_stream_file_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        upload = self._make_one(_Stream(), chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        with self.assertRaises(TransferInvalidError):
            upload.stream_file()

    def test_stream_file_already_complete_w_unseekable_stream(self):
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        http = object()
        stream = object()
        response = object()
        upload = self._make_one(stream, chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        upload._initialize(http, _Request.URL)
        upload._final_response = response
        upload._complete = True
        self.assertIs(upload.stream_file(), response)

    def test_stream_file_already_complete_w_seekable_stream_unsynced(self):
        from google.cloud.streaming.exceptions import CommunicationError
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _Stream(CONTENT)
        response = object()
        upload = self._make_one(stream, chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        upload._initialize(http, _Request.URL)
        upload._final_response = response
        upload._complete = True
        with self.assertRaises(CommunicationError):
            upload.stream_file()

    def test_stream_file_already_complete_wo_seekable_method_synced(self):
        import os
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _Stream(CONTENT)
        stream.seek(0, os.SEEK_END)
        response = object()
        upload = self._make_one(stream, chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        upload._initialize(http, _Request.URL)
        upload._final_response = response
        upload._complete = True
        self.assertIs(upload.stream_file(use_chunks=False), response)

    def test_stream_file_already_complete_w_seekable_method_true_synced(self):
        import os
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _StreamWithSeekableMethod(CONTENT, True)
        stream.seek(0, os.SEEK_END)
        response = object()
        upload = self._make_one(stream, chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        upload._initialize(http, _Request.URL)
        upload._final_response = response
        upload._complete = True
        self.assertIs(upload.stream_file(use_chunks=False), response)

    def test_stream_file_already_complete_w_seekable_method_false(self):
        import os
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _StreamWithSeekableMethod(CONTENT, False)
        stream.seek(0, os.SEEK_END)
        response = object()
        upload = self._make_one(stream, chunksize=1024)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 128
        upload._initialize(http, _Request.URL)
        upload._final_response = response
        upload._complete = True
        self.assertIs(upload.stream_file(use_chunks=False), response)

    def test_stream_file_incomplete(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream, chunksize=6)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 6
        upload._initialize(http, self.UPLOAD_URL)

        info_1 = {'content-length': '0', 'range': 'bytes=0-5'}
        response_1 = _makeResponse(RESUME_INCOMPLETE, info_1)
        info_2 = {'content-length': '0', 'range': 'bytes=6-9'}
        response_2 = _makeResponse(http_client.OK, info_2)
        requester = _MakeRequest(response_1, response_2)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            response = upload.stream_file()

        self.assertIs(response, response_2)
        self.assertEqual(len(requester._responses), 0)
        self.assertEqual(len(requester._requested), 2)

        request_1 = requester._requested[0][0]
        self.assertEqual(request_1.url, self.UPLOAD_URL)
        self.assertEqual(request_1.http_method, 'PUT')
        self.assertEqual(request_1.headers,
                         {'Content-Range': 'bytes 0-5/*',
                          'Content-Type': self.MIME_TYPE})
        self.assertEqual(request_1.body, CONTENT[:6])

        request_2 = requester._requested[1][0]
        self.assertEqual(request_2.url, self.UPLOAD_URL)
        self.assertEqual(request_2.http_method, 'PUT')
        self.assertEqual(request_2.headers,
                         {'Content-Range': 'bytes 6-9/10',
                          'Content-Type': self.MIME_TYPE})
        self.assertEqual(request_2.body, CONTENT[6:])

    def test_stream_file_incomplete_w_transfer_error(self):
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import CommunicationError
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream, chunksize=6)
        upload.strategy = RESUMABLE_UPLOAD
        upload._server_chunk_granularity = 6
        upload._initialize(http, self.UPLOAD_URL)

        info = {
            'content-length': '0',
            'range': 'bytes=0-4',  # simulate error, s.b. '0-5'
        }
        response = _makeResponse(RESUME_INCOMPLETE, info)
        requester = _MakeRequest(response)

        with _Monkey(MUT,
                     Request=_Request,
                     make_api_request=requester):
            with self.assertRaises(CommunicationError):
                upload.stream_file()

        self.assertEqual(len(requester._responses), 0)
        self.assertEqual(len(requester._requested), 1)

        request = requester._requested[0][0]
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        self.assertEqual(request.headers,
                         {'Content-Range': 'bytes 0-5/*',
                          'Content-Type': self.MIME_TYPE})
        self.assertEqual(request.body, CONTENT[:6])

    def test__send_media_request_wo_error(self):
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        CONTENT = b'ABCDEFGHIJ'
        bytes_http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream)
        upload.bytes_http = bytes_http

        headers = {'Content-Range': 'bytes 0-9/10',
                   'Content-Type': self.MIME_TYPE}
        request = _Request(self.UPLOAD_URL, 'PUT', CONTENT, headers)
        info = {'content-length': '0', 'range': 'bytes=0-4'}
        response = _makeResponse(RESUME_INCOMPLETE, info)
        requester = _MakeRequest(response)

        with _Monkey(MUT, make_api_request=requester):
            upload._send_media_request(request, 9)

        self.assertEqual(len(requester._responses), 0)
        self.assertEqual(len(requester._requested), 1)
        used_request, used_http, _ = requester._requested[0]
        self.assertIs(used_request, request)
        self.assertIs(used_http, bytes_http)
        self.assertEqual(stream.tell(), 4)

    def test__send_media_request_w_error(self):
        from six.moves import http_client
        from google.cloud._testing import _Monkey
        from google.cloud.streaming import transfer as MUT
        from google.cloud.streaming.exceptions import HttpError
        from google.cloud.streaming.http_wrapper import RESUME_INCOMPLETE
        from google.cloud.streaming.transfer import RESUMABLE_UPLOAD
        CONTENT = b'ABCDEFGHIJ'
        bytes_http = object()
        http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream)
        upload.strategy = RESUMABLE_UPLOAD
        upload._initialize(http, self.UPLOAD_URL)
        upload.bytes_http = bytes_http

        headers = {'Content-Range': 'bytes 0-9/10',
                   'Content-Type': self.MIME_TYPE}
        request = _Request(self.UPLOAD_URL, 'PUT', CONTENT, headers)
        info_1 = {'content-length': '0', 'range': 'bytes=0-4'}
        response_1 = _makeResponse(http_client.FORBIDDEN, info_1)
        info_2 = {'Content-Length': '0', 'Range': 'bytes=0-4'}
        response_2 = _makeResponse(RESUME_INCOMPLETE, info_2)
        requester = _MakeRequest(response_1, response_2)

        with _Monkey(MUT, Request=_Request, make_api_request=requester):
            with self.assertRaises(HttpError):
                upload._send_media_request(request, 9)

        self.assertEqual(len(requester._responses), 0)
        self.assertEqual(len(requester._requested), 2)
        first_request, first_http, _ = requester._requested[0]
        self.assertIs(first_request, request)
        self.assertIs(first_http, bytes_http)
        second_request, second_http, _ = requester._requested[1]
        self.assertEqual(second_request.url, self.UPLOAD_URL)
        self.assertEqual(second_request.http_method, 'PUT')  # ACK!
        self.assertEqual(second_request.headers,
                         {'Content-Range': 'bytes */*'})
        self.assertIs(second_http, http)

    def test__send_media_body_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        upload = self._make_one(_Stream())
        with self.assertRaises(TransferInvalidError):
            upload._send_media_body(0)

    def test__send_media_body_wo_total_size(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        http = object()
        upload = self._make_one(_Stream())
        upload._initialize(http, _Request.URL)
        with self.assertRaises(TransferInvalidError):
            upload._send_media_body(0)

    def test__send_media_body_start_lt_total_size(self):
        from google.cloud.streaming.stream_slice import StreamSlice
        SIZE = 1234
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=SIZE)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer

        found = upload._send_media_body(0)

        self.assertIs(found, response)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        body_stream = request.body
        self.assertIsInstance(body_stream, StreamSlice)
        self.assertIs(body_stream._stream, stream)
        self.assertEqual(len(body_stream), SIZE)
        self.assertEqual(request.headers,
                         {'content-length': '%d' % (SIZE,),  # speling!
                          'Content-Type': self.MIME_TYPE,
                          'Content-Range': 'bytes 0-%d/%d' % (SIZE - 1, SIZE)})
        self.assertEqual(end, SIZE)

    def test__send_media_body_start_eq_total_size(self):
        from google.cloud.streaming.stream_slice import StreamSlice
        SIZE = 1234
        http = object()
        stream = _Stream()
        upload = self._make_one(stream, total_size=SIZE)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer

        found = upload._send_media_body(SIZE)

        self.assertIs(found, response)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        body_stream = request.body
        self.assertIsInstance(body_stream, StreamSlice)
        self.assertIs(body_stream._stream, stream)
        self.assertEqual(len(body_stream), 0)
        self.assertEqual(request.headers,
                         {'content-length': '0',  # speling!
                          'Content-Type': self.MIME_TYPE,
                          'Content-Range': 'bytes */%d' % (SIZE,)})
        self.assertEqual(end, SIZE)

    def test__send_chunk_not_initialized(self):
        from google.cloud.streaming.exceptions import TransferInvalidError
        upload = self._make_one(_Stream())
        with self.assertRaises(TransferInvalidError):
            upload._send_chunk(0)

    def test__send_chunk_wo_total_size_stream_exhausted(self):
        CONTENT = b'ABCDEFGHIJ'
        SIZE = len(CONTENT)
        http = object()
        upload = self._make_one(_Stream(CONTENT), chunksize=1000)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer
        self.assertIsNone(upload.total_size)

        found = upload._send_chunk(0)

        self.assertIs(found, response)
        self.assertEqual(upload.total_size, SIZE)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        self.assertEqual(request.body, CONTENT)
        self.assertEqual(request.headers,
                         {'content-length': '%d' % SIZE,  # speling!
                          'Content-Type': self.MIME_TYPE,
                          'Content-Range': 'bytes 0-%d/%d' % (SIZE - 1, SIZE)})
        self.assertEqual(end, SIZE)

    def test__send_chunk_wo_total_size_stream_not_exhausted(self):
        CONTENT = b'ABCDEFGHIJ'
        SIZE = len(CONTENT)
        CHUNK_SIZE = SIZE - 5
        http = object()
        upload = self._make_one(_Stream(CONTENT), chunksize=CHUNK_SIZE)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer
        self.assertIsNone(upload.total_size)

        found = upload._send_chunk(0)

        self.assertIs(found, response)
        self.assertIsNone(upload.total_size)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        self.assertEqual(request.body, CONTENT[:CHUNK_SIZE])
        expected_headers = {
            'content-length': '%d' % CHUNK_SIZE,  # speling!
            'Content-Type': self.MIME_TYPE,
            'Content-Range': 'bytes 0-%d/*' % (CHUNK_SIZE - 1,),
        }
        self.assertEqual(request.headers, expected_headers)
        self.assertEqual(end, CHUNK_SIZE)

    def test__send_chunk_w_total_size_stream_not_exhausted(self):
        from google.cloud.streaming.stream_slice import StreamSlice
        CONTENT = b'ABCDEFGHIJ'
        SIZE = len(CONTENT)
        CHUNK_SIZE = SIZE - 5
        http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream, total_size=SIZE, chunksize=CHUNK_SIZE)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer

        found = upload._send_chunk(0)

        self.assertIs(found, response)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        body_stream = request.body
        self.assertIsInstance(body_stream, StreamSlice)
        self.assertIs(body_stream._stream, stream)
        self.assertEqual(len(body_stream), CHUNK_SIZE)
        expected_headers = {
            'content-length': '%d' % CHUNK_SIZE,  # speling!
            'Content-Type': self.MIME_TYPE,
            'Content-Range': 'bytes 0-%d/%d' % (CHUNK_SIZE - 1, SIZE),
        }
        self.assertEqual(request.headers, expected_headers)
        self.assertEqual(end, CHUNK_SIZE)

    def test__send_chunk_w_total_size_stream_exhausted(self):
        from google.cloud.streaming.stream_slice import StreamSlice
        CONTENT = b'ABCDEFGHIJ'
        SIZE = len(CONTENT)
        CHUNK_SIZE = 1000
        http = object()
        stream = _Stream(CONTENT)
        upload = self._make_one(stream, total_size=SIZE, chunksize=CHUNK_SIZE)
        upload._initialize(http, self.UPLOAD_URL)
        response = object()
        streamer = _MediaStreamer(response)
        upload._send_media_request = streamer

        found = upload._send_chunk(SIZE)

        self.assertIs(found, response)
        request, end = streamer._called_with
        self.assertEqual(request.url, self.UPLOAD_URL)
        self.assertEqual(request.http_method, 'PUT')
        body_stream = request.body
        self.assertIsInstance(body_stream, StreamSlice)
        self.assertIs(body_stream._stream, stream)
        self.assertEqual(len(body_stream), 0)
        self.assertEqual(request.headers,
                         {'content-length': '0',  # speling!
                          'Content-Type': self.MIME_TYPE,
                          'Content-Range': 'bytes */%d' % (SIZE,)})
        self.assertEqual(end, SIZE)


def _email_chunk_parser():
    import six
    if six.PY3:  # pragma: NO COVER  Python3
        from email.parser import BytesParser
        parser = BytesParser()
        return parser.parsebytes
    else:
        from email.parser import Parser
        parser = Parser()
        return parser.parsestr


class _Dummy(object):
    def __init__(self, **kw):
        self.__dict__.update(kw)


class _UploadConfig(object):
    accept = ('*/*',)
    max_size = None
    resumable_path = '/resumable/endpoint'
    simple_multipart = True
    simple_path = '/upload/endpoint'


class _Stream(object):
    _closed = False

    def __init__(self, to_read=b''):
        import io
        self._written = []
        self._to_read = io.BytesIO(to_read)

    def write(self, to_write):
        self._written.append(to_write)

    def seek(self, offset, whence=0):
        self._to_read.seek(offset, whence)

    def read(self, size=None):
        if size is not None:
            return self._to_read.read(size)
        return self._to_read.read()

    def tell(self):
        return self._to_read.tell()

    def close(self):
        self._closed = True


class _StreamWithSeekableMethod(_Stream):

    def __init__(self, to_read=b'', seekable=True):
        super(_StreamWithSeekableMethod, self).__init__(to_read)
        self._seekable = seekable

    def seekable(self):
        return self._seekable


class _Request(object):
    __slots__ = ('url', 'http_method', 'body', 'headers', 'loggable_body')
    URL = 'http://example.com/api'

    def __init__(self, url=URL, http_method='GET', body='', headers=None):
        self.url = url
        self.http_method = http_method
        self.body = self.loggable_body = body
        if headers is None:
            headers = {}
        self.headers = headers


class _MakeRequest(object):

    def __init__(self, *responses):
        self._responses = list(responses)
        self._requested = []

    def __call__(self, http, request, **kw):
        self._requested.append((request, http, kw))
        return self._responses.pop(0)


def _makeResponse(status_code, info=None, content='',
                  request_url=_Request.URL):
    if info is None:
        info = {}
    return _Dummy(status_code=status_code,
                  info=info,
                  content=content,
                  length=len(content),
                  request_url=request_url)


class _MediaStreamer(object):

    _called_with = None

    def __init__(self, response):
        self._response = response

    def __call__(self, request, end):
        assert self._called_with is None
        self._called_with = (request, end)
        return self._response
