# Copyright (C) 2017 The Meme Factory, Inc.  http://www.meme.com/
#
# 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.

# Karl O. Pinc <kop@meme.com>

import errno

import googleapiclient.discovery
import googleapiclient.errors
import httplib2
import oauth2client.client
import oauth2client.service_account
import pytest

from enforcer import get_rules
from enforcer import exceptions as ex

import libtest


# Constants

INVALID_JSON_PATH = 'tests/data/bad_credentials.json'


# Helper classes

class MockServiceAccountCredentials(object):
    @classmethod
    def from_json_keyfile_name(cls, file, scopes):
        return (file, scopes)


class MockCredsIdentity(object):
    def authorize(self, arg):
        return arg


class MockCredsRaiseOauth2(object):
    def authorize(self, arg):
        raise oauth2client.client.HttpAccessTokenRefreshError()


class MockDrive(object):
    def __init__(self):
        super(MockDrive, self).__init__()
        self.default = None
        self.file_id = None
        self.mime_type = None
        self.q = None
        self.order_by = None
        self.type = None

    def files(self):
        return self

    def list(self, q=None, orderBy=None, fields=None):
        self.q = q
        self.order_by = orderBy
        self.fields = fields
        return self

    def execute(self):
        return self

    def export(self, fileId=None, mimeType=None):
        self.file_id = fileId
        self.mime_type = mimeType
        return self

    def get(self, type, default):
        self.type = type
        self.default = default
        return self

    def decode(self, encoding='utf-8', errors='strict'):
        return self


# Helper functions

def mock_google_credentials(json_file):
    return MockCredsIdentity()


def mock_authorize_connection(creds):
    return creds.authorize(httplib2.Http())


# google_credentials()

def test_google_credentials_success(monkeypatch):
    '''The google credentials are called properly'''
    monkeypatch.setattr(
        oauth2client.service_account,
        'ServiceAccountCredentials',
        MockServiceAccountCredentials)
    file = 'some file'
    assert get_rules.google_credentials(file) == (file, get_rules.SCOPES)


def test_google_credentials_file_existance():
    '''A non-existant json file raises an exception'''
    with pytest.raises(ex.NoGoogleCredentialsFileError):
        get_rules.google_credentials('no_such.json')


def test_google_credentials_file_permissions(monkeypatch):
    '''Bad permissions on the json credentials file raises an error'''
    class MockServiceAccountCredentialsRaise(object):
        @classmethod
        def from_json_keyfile_name(cls, file, scopes):
            raise IOError(errno.EPERM, 'bogus permission error')

    monkeypatch.setattr(
        oauth2client.service_account,
        'ServiceAccountCredentials',
        MockServiceAccountCredentialsRaise)
    file = 'some file'
    with pytest.raises(ex.InsufficientPermissionsError):
        get_rules.google_credentials(file)


def test_google_credentials_other_ioerror(monkeypatch):
    '''"Other" ioerror on json credentials file raises itself'''
    class MockServiceAccountCredentialsRaise(object):
        @classmethod
        def from_json_keyfile_name(cls, file, scopes):
            raise IOError(errno.ENOSPC, 'bogus disk full error')

    monkeypatch.setattr(
        oauth2client.service_account,
        'ServiceAccountCredentials',
        MockServiceAccountCredentialsRaise)
    file = 'some file'
    with pytest.raises(IOError):
        get_rules.google_credentials(file)


def test_google_credentials_not_json():
    '''A non-json file raises an exception'''
    with pytest.raises(ex.NotAServiceAccountError):
        get_rules.google_credentials(libtest.TEST_CONFIG_PATH)


def test_google_credentials_bad_json():
    '''A bad json file raises an exception'''
    with pytest.raises(ex.InvalidGoogleCredentialsError):
        get_rules.google_credentials(INVALID_JSON_PATH)


# authorize_connection()

def test_authorize_connection_success():
    '''Authorizing a connection returns a http object'''
    assert isinstance(get_rules.authorize_connection(MockCredsIdentity()),
                      httplib2.Http)


def test_authorize_connection_fail():
    '''Failing to authorize a connection raises an error'''
    with pytest.raises(ex.GoogleAuthorizationError):
        get_rules.authorize_connection(MockCredsRaiseOauth2())


# build_drive()

def test_build_drive_success(monkeypatch):
    '''The Google API build function is called with the correct args'''
    def mock_build(api, version, http=None):
        return (api, version, http)

    monkeypatch.setattr(googleapiclient.discovery, 'build', mock_build)
    (api, version, http) = get_rules.build_drive(MockCredsIdentity(),
                                                 mock_authorize_connection)

    assert api == get_rules.DRIVE_API
    assert version == get_rules.DRIVE_VERSION
    assert isinstance(http, httplib2.Http)


def test_build_drive_fail_oauth2():
    '''The Google API build function raises an error when oauth2 fails'''
    with pytest.raises(ex.GoogleDriveConnectionError):
        get_rules.build_drive(
            MockCredsRaiseOauth2(), mock_authorize_connection)


def test_build_drive_fail_googleapiclient(monkeypatch):
    '''The Google API build function raises an error when the api fails'''
    def mock_build(api, version, http=None):
        raise googleapiclient.errors.Error()

    monkeypatch.setattr(googleapiclient.discovery, 'build', mock_build)

    with pytest.raises(ex.GoogleDriveConnectionError):
        get_rules.build_drive(MockCredsIdentity(), mock_authorize_connection)


# get_drive_file_info()

def test_get_drive_file_info_success():
    '''We call the google drive object with the expected arguments'''
    mock_drive = MockDrive()
    name = 'some name'
    get_rules.get_drive_file_info(mock_drive, name)

    assert mock_drive.q is not None
    assert mock_drive.order_by is not None
    assert mock_drive.fields is not None
    assert mock_drive.type is not None
    assert mock_drive.default is not None


def test_get_drive_file_info_fail_drive_goodname():
    '''We get an exception when google drive api raises one although the
    supplied name is good'''
    class MockDriveRaiseClient(MockDrive):
        def get(self, type, default):
            raise googleapiclient.errors.Error()

    with pytest.raises(ex.GoogleListError):
        get_rules.get_drive_file_info(MockDriveRaiseClient(), 'some name')


def test_get_drive_file_info_fail_drive_badname(monkeypatch):
    '''We get an exception when google drive api raises one when the
    supplied name is bad'''
    class MockHttpError(googleapiclient.errors.Error):
        def __init__(self):
            self.resp = {'status': '400'}

    class MockDriveRaiseClient(MockDrive):
        def get(self, type, default):
            err = MockHttpError()
            raise err

    monkeypatch.setattr(googleapiclient.errors, 'HttpError', MockHttpError)

    with pytest.raises(ex.InvalidGoogleListCallError):
        get_rules.get_drive_file_info(MockDriveRaiseClient(), 'some name')


def test_get_drive_file_info_fail_oauth2():
    '''We get an exception when google oauth2 api raises one'''
    class MockDriveRaiseOauth2(MockDrive):
        def get(self, type, default):
            raise oauth2client.client.Error()

    with pytest.raises(ex.GoogleListError):
        get_rules.get_drive_file_info(MockDriveRaiseOauth2(), 'some name')


# download_sheet()

def test_download_sheet_success():
    '''The google drive object is called as expected'''
    mock_drive = MockDrive()
    id = 'some id'
    name = 'some name'
    get_rules.download_sheet(mock_drive, id, name)

    assert mock_drive.file_id == id
    assert mock_drive.mime_type == get_rules.DST_MIMETYPE


def test_download_sheet_fail_drive():
    '''We get an exception when google drive api raises one'''
    class MockDriveRaiseClient(MockDrive):
        def execute(self):
            raise googleapiclient.errors.Error()

    with pytest.raises(ex.GoogleDownloadError):
        get_rules.download_sheet(
            MockDriveRaiseClient(), 'some id', 'some name')


def test_download_sheet_fail_oauth2():
    '''We get an exception when google oauth2 api raises one'''
    class MockDriveRaiseOauth2(MockDrive):
        def execute(self):
            raise oauth2client.client.Error()

    with pytest.raises(ex.GoogleDownloadError):
        get_rules.download_sheet(
            MockDriveRaiseOauth2(), 'some id', 'some name')


def test_download_sheet_fail_content():
    '''We get an exception when google returns no spreadsheet'''
    class MockDriveNone(MockDrive):
        def execute(self):
            return None

    with pytest.raises(ex.NoSheetDownloadedError):
        get_rules.download_sheet(
            MockDriveNone(), 'some id', 'some name')


# rules_from_google()

def test_rules_from_google_success(monkeypatch):
    '''We get something back when the download is successful'''
    class MockDriveData(MockDrive):
        def decode(self, encoding='utf-8', errors='strict'):
            return 'MockDriveData()\r\n'

        def get(self, type, default):
            return [{'id': 'some id'}]

        def split(self, value):
            return self

    def mock_build(api, version, http=None):
        return MockDriveData()

    monkeypatch.setattr(
        get_rules, 'google_credentials' , mock_google_credentials)
    monkeypatch.setattr(googleapiclient.discovery, 'build', mock_build)

    (metainfo, rules) = get_rules.rules_from_google(
        'creds.json', mock_authorize_connection, 'some name')
    assert rules == ['MockDriveData()\r\n']


def test_rules_from_google_fail(monkeypatch):
    '''We get an exception when the spreadsheet is not found by google'''
    class MockDriveNoSheet(MockDrive):
        def get(self, type, default):
            return []

    def mock_build(api, version, http=None):
        return MockDriveNoSheet()

    monkeypatch.setattr(
        get_rules, 'google_credentials' , mock_google_credentials)
    monkeypatch.setattr(googleapiclient.discovery, 'build', mock_build)

    with pytest.raises(ex.NoSuchSpreadsheetError):
        get_rules.rules_from_google(
            'creds.json', mock_authorize_connection, 'some name')


def test_rules_from_google_httperror(monkeypatch):
    '''We get an exception when httplib2 raises an error'''
    def mock_build_drive(creds, authorize):
        raise httplib2.HttpLib2Error('test exception')

    monkeypatch.setattr(
        get_rules, 'google_credentials' , mock_google_credentials)
    monkeypatch.setattr(get_rules, 'build_drive', mock_build_drive)

    with pytest.raises(ex.HttpLib2Error):
        get_rules.rules_from_google('creds.json', None, 'some name')


# rules_from_file()

def test_rules_from_file_first():
    '''The first 2 lines of a rule file can be read'''
    (metainfo, data) = get_rules.rules_from_file('sample_rules.csv')
    assert data[0:2] == ['line 1\n', 'line 2\n']


def test_rules_from_file_last():
    '''The last line of a rule file can be read'''
    (metainfo, data) = get_rules.rules_from_file('sample_rules.csv')
    assert data[-1] == 'last line\n'


def test_rule_file_existance():
    '''A non-existant rule file raises an exception'''
    with pytest.raises(ex.NoRulesFileError):
        get_rules.rules_from_file('no_such.csv')


# get_rule_file()

def test_get_rule_file_file(monkeypatch):
    '''Return rules from a file'''
    def mock_rules_from_file(path):
        return path

    monkeypatch.setattr(get_rules, 'rules_from_file', mock_rules_from_file)

    path = 'some/path'
    assert (get_rules.get_rule_file({'rules_source': get_rules.FILE_SOURCE,
                                     'rules_path': path}) ==
            path)


def test_get_rule_file_google(monkeypatch):
    '''Return rules from google'''
    def mock_rules_from_google(creds, authorize, name):
        return (creds, authorize, name)

    monkeypatch.setattr(get_rules, 'rules_from_google', mock_rules_from_google)

    creds = 'some.json'
    name = 'some name'
    assert (get_rules.get_rule_file(
        {'rules_source': get_rules.GOOGLE_SOURCE,
         'google_credentials': creds,
         'spreadsheet_name': name}) ==
        (creds, get_rules.authorize_connection, name))
