from unittest.mock import patch
import base64
import hashlib
import io
import json
import pytest
from pytest_lazyfixture import lazy_fixture

from boxsdk.exception import BoxAPIException
from boxsdk.config import API
from boxsdk.util.chunked_uploader import ChunkedUploader
from boxsdk.object.file import File
from boxsdk.object.upload_session import UploadSession


UPLOAD_SESSION_ID = 'F971964745A5CD0C001BBE4E58196BFD'
SESSION_ENDPOINTS = {
    "abort": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}",
    "commit": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}/commit",
    "list_parts": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}/parts",
    "log_event": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}/log",
    "status": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}",
    "upload_part": f"https://changed.box.com/api/2.0/files/upload_sessions/{UPLOAD_SESSION_ID}"
}


@pytest.fixture()
def upload_session_using_upload_session_urls(mock_box_session):
    upload_session_response_object = {
        'part_size': 8,
        'total_parts': 10,
        'session_endpoints': SESSION_ENDPOINTS,
    }
    return UploadSession(
        mock_box_session,
        UPLOAD_SESSION_ID,
        upload_session_response_object,
        use_upload_session_urls=True
    )


@pytest.fixture()
def upload_session_not_using_upload_session_urls(mock_box_session):
    upload_session_response_object = {
        'part_size': 8,
        'total_parts': 10,
        'session_endpoints': SESSION_ENDPOINTS,
    }
    return UploadSession(
        mock_box_session,
        UPLOAD_SESSION_ID,
        upload_session_response_object,
        use_upload_session_urls=False
    )


@pytest.mark.parametrize(
    'test_upload_session, expected_url',
    [
        (lazy_fixture('upload_session_using_upload_session_urls'), SESSION_ENDPOINTS['list_parts']),
        (lazy_fixture('upload_session_not_using_upload_session_urls'),
         f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/parts')
    ])
def test_get_parts(mock_box_session, test_upload_session, expected_url):
    mock_entry = {
        'part_id': '8F0966B1',
        'offset': 0,
        'size': 8,
        'sha1': None,
    }
    mock_box_session.get.return_value.json.return_value = {
        'entries': [mock_entry],
        'offset': 0,
        'total_count': 1,
        'limit': 1000,
    }
    test_parts = test_upload_session.get_parts()
    part = test_parts.next()
    mock_box_session.get.assert_called_once_with(expected_url, params={'offset': None})
    assert isinstance(part, dict)
    assert part['part_id'] == mock_entry['part_id']
    assert part['size'] == mock_entry['size']
    assert part['offset'] == mock_entry['offset']


@pytest.mark.parametrize(
    'test_upload_session, expected_url',
    [
        (lazy_fixture('upload_session_using_upload_session_urls'), SESSION_ENDPOINTS['abort']),
        (lazy_fixture('upload_session_not_using_upload_session_urls'),
         f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}')
    ])
def test_abort(mock_box_session, test_upload_session, expected_url):
    mock_box_session.delete.return_value.ok = True
    result = test_upload_session.abort()
    mock_box_session.delete.assert_called_once_with(expected_url, expect_json_response=False)
    assert result is True


@pytest.mark.parametrize(
    'test_upload_session, expected_url',
    [
        (lazy_fixture('upload_session_using_upload_session_urls'), SESSION_ENDPOINTS['upload_part']),
        (lazy_fixture('upload_session_not_using_upload_session_urls'),
         f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}')
    ])
def test_upload_part_bytes(mock_box_session, test_upload_session, expected_url):
    part_bytes = b'abcdefgh'
    offset = 32
    total_size = 80
    expected_sha1 = 'QlrxKgdDUCsyLpOgFbz4aOMk1Wo='
    expected_headers = {
        'Content-Type': 'application/octet-stream',
        'Digest': f'SHA={expected_sha1}',
        'Content-Range': 'bytes 32-39/80',
    }
    mock_box_session.put.return_value.json.return_value = {
        'part': {
            'part_id': 'ABCDEF123',
            'offset': offset,
            'size': 8,
            'sha1': expected_sha1,
        },
    }
    part = test_upload_session.upload_part_bytes(part_bytes, offset, total_size)

    mock_box_session.put.assert_called_once_with(expected_url, data=part_bytes, headers=expected_headers)
    assert isinstance(part, dict)
    assert part['sha1'] == expected_sha1
    assert part['size'] == 8
    assert part['part_id'] == 'ABCDEF123'


@pytest.mark.parametrize(
    'test_upload_session, expected_url',
    [
        (lazy_fixture('upload_session_using_upload_session_urls'), SESSION_ENDPOINTS['commit']),
        (lazy_fixture('upload_session_not_using_upload_session_urls'),
         f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/commit')
    ])
def test_commit(mock_box_session, test_upload_session, expected_url):
    sha1 = hashlib.sha1()
    sha1.update(b'fake_file_data')
    file_id = '12345'
    file_type = 'file'
    file_etag = '7'
    file_attributes = {'description': 'This is a test description.'}
    parts = [
        {
            'part_id': 'ABCDEF123',
            'offset': 0,
            'size': 8,
            'sha1': 'fake_sha1',
        },
        {
            'part_id': 'ABCDEF456',
            'offset': 8,
            'size': 8,
            'sha1': 'fake_sha1',
        },
    ]
    expected_data = {
        'attributes': file_attributes,
        'parts': parts,
    }
    expected_headers = {
        'Content-Type': 'application/json',
        'Digest': f'SHA={base64.b64encode(sha1.digest()).decode("utf-8")}',
        'If-Match': '7',
    }
    mock_box_session.post.return_value.json.return_value = {
        'entries': [
            {
                'type': file_type,
                'id': file_id,
                'description': 'This is a test description.',
            },
        ],
    }
    created_file = test_upload_session.commit(
        content_sha1=sha1.digest(), parts=parts, file_attributes=file_attributes, etag=file_etag
    )
    mock_box_session.post.assert_called_once_with(
        expected_url, data=json.dumps(expected_data), headers=expected_headers
    )
    assert isinstance(created_file, File)
    assert created_file.id == file_id
    assert created_file.type == file_type
    assert created_file.description == 'This is a test description.'


@pytest.mark.parametrize(
    'test_upload_session, expected_get_url, expected_commit_url',
    [
        (
            lazy_fixture('upload_session_using_upload_session_urls'),
            SESSION_ENDPOINTS['list_parts'],
            SESSION_ENDPOINTS['commit']
        ),
        (
            lazy_fixture('upload_session_not_using_upload_session_urls'),
            f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/parts',
            f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/commit'
        )
    ])
def test_commit_with_missing_params(mock_box_session, test_upload_session, expected_get_url, expected_commit_url):
    sha1 = hashlib.sha1()
    sha1.update(b'fake_file_data')
    file_id = '12345'
    file_type = 'file'
    parts = [
        {
            'part_id': '8F0966B1',
            'offset': 0,
            'size': 8,
            'sha1': None,
        },
    ]
    expected_data = {
        'parts': parts,
    }
    expected_headers = {
        'Content-Type': 'application/json',
        'Digest': f'SHA={base64.b64encode(sha1.digest()).decode("utf-8")}',
    }
    mock_entry = {
        'part_id': '8F0966B1',
        'offset': 0,
        'size': 8,
        'sha1': None,
    }
    mock_box_session.get.return_value.json.return_value = {
        'entries': [mock_entry],
        'offset': 0,
        'total_count': 1,
        'limit': 1000,
    }
    mock_box_session.post.return_value.json.return_value = {
        'entries': [
            {
                'type': file_type,
                'id': file_id,
            },
        ],
    }
    created_file = test_upload_session.commit(content_sha1=sha1.digest())
    mock_box_session.get.assert_called_once_with(expected_get_url, params={'offset': None})
    mock_box_session.post.assert_called_once_with(
        expected_commit_url, data=json.dumps(expected_data), headers=expected_headers
    )
    assert isinstance(created_file, File)
    assert created_file.id == file_id
    assert created_file.type == file_type


@pytest.mark.parametrize(
    'test_upload_session, expected_url',
    [
        (lazy_fixture('upload_session_using_upload_session_urls'), SESSION_ENDPOINTS['commit']),
        (lazy_fixture('upload_session_not_using_upload_session_urls'),
         f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/commit')
    ])
def test_commit_returns_none_when_202_is_returned(mock_box_session, test_upload_session, expected_url):
    sha1 = hashlib.sha1()
    sha1.update(b'fake_file_data')
    file_etag = '7'
    file_attributes = {'description': 'This is a test description.'}
    parts = [
        {
            'part_id': 'ABCDEF123',
            'offset': 0,
            'size': 8,
            'sha1': 'fake_sha1',
        },
        {
            'part_id': 'ABCDEF456',
            'offset': 8,
            'size': 8,
            'sha1': 'fake_sha1',
        },
    ]
    expected_data = {
        'attributes': file_attributes,
        'parts': parts,
    }
    expected_headers = {
        'Content-Type': 'application/json',
        'Digest': f'SHA={base64.b64encode(sha1.digest()).decode("utf-8")}',
        'If-Match': '7',
    }
    mock_box_session.post.side_effect = BoxAPIException(status=202)

    created_file = test_upload_session.commit(
        content_sha1=sha1.digest(), parts=parts, file_attributes=file_attributes, etag=file_etag
    )

    mock_box_session.post.assert_called_once_with(
        expected_url, data=json.dumps(expected_data), headers=expected_headers
    )
    assert created_file is None


@pytest.mark.parametrize(
    'test_upload_session',
    [
        lazy_fixture('upload_session_using_upload_session_urls'),
        lazy_fixture('upload_session_not_using_upload_session_urls'),
    ])
def test_get_chunked_uploader_for_stream(test_upload_session):
    file_size = 197520
    part_bytes = b'abcdefgh'
    stream = io.BytesIO(part_bytes)
    chunked_uploader = test_upload_session.get_chunked_uploader_for_stream(stream, file_size)
    assert isinstance(chunked_uploader, ChunkedUploader)


@pytest.mark.parametrize(
    'test_upload_session',
    [
        lazy_fixture('upload_session_using_upload_session_urls'),
        lazy_fixture('upload_session_not_using_upload_session_urls'),
    ])
def test_get_chunked_uploader(mock_content_response, mock_file_path, test_upload_session):
    mock_file_stream = io.BytesIO(mock_content_response.content)
    file_size = 197520
    with patch('os.stat') as stat:
        stat.return_value.st_size = file_size
        with patch('boxsdk.object.upload_session.open', return_value=mock_file_stream):
            chunked_uploader = test_upload_session.get_chunked_uploader(mock_file_path)
    assert isinstance(chunked_uploader, ChunkedUploader)


def test_get_url_do_not_use_session_urls_if_base_url_was_changed(upload_session_using_upload_session_urls):
    old_base_upload_url = API.UPLOAD_URL
    new_base_upload_url = 'https://new-upload.box.com/api/2.0'
    API.UPLOAD_URL = new_base_upload_url
    try:
        url = upload_session_using_upload_session_urls.get_url('commit', url_key='commit')
        assert url == f'{new_base_upload_url}/files/upload_sessions/{UPLOAD_SESSION_ID}/commit'
        assert url != SESSION_ENDPOINTS['commit']
    finally:
        API.UPLOAD_URL = old_base_upload_url


def test_get_url_uses_session_urls_if_base_url_was_not_changed(upload_session_using_upload_session_urls):
    url = upload_session_using_upload_session_urls.get_url('commit', url_key='commit')
    assert url != f'{API.UPLOAD_URL}/files/upload_sessions/{UPLOAD_SESSION_ID}/commit'
    assert url == SESSION_ENDPOINTS['commit']
