import datetime
import unittest
import pandas as pd
import pytest
import responses

from datarobot import errors
from datarobot.client import Client, set_client
from datarobot.errors import DataRobotDeprecationWarning
from datarobot.utils import (dataframe_to_buffer, parse_time, get_id_from_response,
                             from_api, deprecation, recognize_sourcedata, to_api, is_urlsource)
from datarobot.utils.sourcedata import list_of_records_to_buffer
from tests.test_helpers import fixture_file_path
from tests.utils import warns


class TestDataframeSerialization(unittest.TestCase):

    def test_list_of_records_to_buffer(self):
        records = [{'a': 'b', 'c': 'd'}, {'a': 'x', 'c': 'y'}]
        expected = "a,c\r\nb,d\r\nx,y\r\n"
        buff = list_of_records_to_buffer(records)
        self.assertEqual(buff.read(), expected)

    def test_no_index_please(self):
        df = pd.DataFrame({'a': range(100), 'b': range(100)})
        buff = dataframe_to_buffer(df)
        readback = pd.read_csv(buff)
        self.assertEqual(readback.columns.tolist(), ['a', 'b'])

    def test_parse_time(self):
        self.assertEqual('BAD TIME', parse_time('BAD TIME'))  # returns value
        test_string_time = datetime.datetime.now().isoformat()
        self.assertIsInstance(test_string_time, str)
        self.assertIsInstance(parse_time(test_string_time), datetime.datetime)

    @responses.activate
    @pytest.mark.usefixtures('no_client_version_check')
    def test_get_id_from_response_location_header(self):
        responses.add(responses.POST,
                      'http://nothing/',
                      body='',
                      adding_headers={'Location': 'http://nothing/f-id/'})
        client = set_client(Client(token='no_matter',
                                   endpoint='http://nothing'))
        resp = client.post('')
        self.assertEqual(get_id_from_response(resp), 'f-id')

    def test_csv_buffer_is_quoted(self):
        """"
        This tests that dataframes are correctly quoted when saved back to csv, so
        that columns containing line endings won't be interpreted as multiple rows
        See: PRED-3203
        """
        df = pd.DataFrame({'A': ['test\rtest', 2], 'B': ['test\n\n\n\r\ntest', 0.75]})
        buff = dataframe_to_buffer(df)
        readback = pd.read_csv(buff)
        assert df.shape[0] == readback.shape[0]


class TestFromAPI(unittest.TestCase):

    def test_nested_list_of_objects_all_changed(self):
        source = {
            'oneFish': [
                {'twoFish': 'redFish'},
                {'blueFish': 'noFish'}
            ]
        }
        result = from_api(source)
        inner = result['one_fish']
        self.assertEqual(inner[0]['two_fish'], 'redFish')
        self.assertEqual(inner[1]['blue_fish'], 'noFish')

    def test_nested_objects_all_changed(self):
        source = {
            'oneFish': {
                'twoFish': 'redFish'
            }
        }

        result = from_api(source)
        self.assertEqual(result['one_fish']['two_fish'], 'redFish')


def test_from_api_filter_nones():
    data_in = {
        'someValue': {
            'nestedNone': None,
            'nestedNotNone': ''
        },
        'listNone': [{
            'someValue': None,
            'someNotNone': ''
        }],
        'otherNone': None
    }
    data_out = {
        'some_value': {'nested_not_none': ''},
        'list_none': [{'some_not_none': ''}]
    }
    assert data_out == from_api(data_in)


def test_from_api_no_filter_nones():
    data_in = {
        'someValue': {
            'nestedNone': None,
            'nestedNotNone': ''
        },
        'listNone': [{
            'someValue': None,
            'someNotNone': ''
        }],
        'otherNone': None
    }
    data_out = {
        'some_value': {
            'nested_none': None,
            'nested_not_none': ''
        },
        'list_none': [{
            'some_value': None,
            'some_not_none': ''
        }],
        'other_none': None
    }
    assert data_out == from_api(data_in, keep_null_keys=True)


def test_from_api_keep_nones():
    data_in = {
        'someValue': {
            'nestedNone': None,
            'nestedNotNone': '',
            'levelTwo': {
                'levelThree': None
            }
        },
        'otherNone': None,
        'keepLevelOne': None
    }
    data_out = {
        'some_value': {
            'nested_not_none': '',
            'nested_none': None,
            'level_two': {
                'level_three': None
            }
        },
        'keep_level_one': None
    }
    assert data_out == from_api(data_in, keep_attrs=[
        'some_value.nested_none',
        'some_value.level_two.level_three',
        'keep_level_one'
    ])


def test_from_api_multiple_underscore_ignored():
    in_data = {'samplePct__gte': 64}
    expected = {'sample_pct__gte': 64}
    assert from_api(in_data) == expected


def test_to_api_multiple_underscore_ignored():
    in_data = {'sample_pct__gte': 64}
    expected = {'samplePct__gte': 64}
    assert to_api(in_data) == expected


def test_from_api_single_letter():
    in_data = {'iAmAKey': 'test'}
    expected = {'i_am_a_key': 'test'}
    assert from_api(in_data) == expected


def test_to_api_single_letter():
    in_data = {'i_am_a_key': 'test'}
    expected = {'iAmAKey': 'test'}
    assert to_api(in_data) == expected


@deprecation.deprecated(deprecated_since_version='v0.1.2',
                        will_remove_version='v1.2.3')
def bar(arg1, arg2):
    """
    A dummy function to use for testing the deprecation mechanisms

    Parameters
    ----------
    arg1 : int
    arg2 : int

    Returns
    -------
    total : int
        arg1 + arg2

    """
    return arg1 + arg2


@deprecation.deprecated(deprecated_since_version='v0.1.2',
                        will_remove_version='v1.2.3',
                        message='Use of `bar` is recommended instead.')
def foo(arg1, arg2):
    """
    A dummy function to use for testing the deprecation mechanisms

    Parameters
    ----------
    arg1 : int
    arg2 : int

    Returns
    -------
    total : int
        foo + bar

    """
    return arg1 + arg2


def test_deprecation_with_message(known_warning):
    foo(1, 2)
    w_msg = known_warning[0].message.args[0]
    assert w_msg == \
        '`foo` has been deprecated in `v0.1.2`, will be removed ' \
        'in `v1.2.3`. Use of `bar` is recommended instead.'


def test_deprecation_message(known_warning):
    bar(1, 2)
    w_instance = known_warning[0].message
    assert isinstance(w_instance, DataRobotDeprecationWarning)


class TestSourcedataUtils(unittest.TestCase):
    def setUp(self):
        self.default_fname = 'predict.csv'

    def test_recognize_sourcedata_passed_dataframe(self):
        df = pd.DataFrame({'a': range(100), 'b': range(100)})
        kwargs = recognize_sourcedata(df, self.default_fname)
        self.assertTrue('filelike' in kwargs)
        self.assertEqual(kwargs.get('fname'), self.default_fname)
        buff = kwargs['filelike']
        readback = pd.read_csv(buff)
        self.assertEqual(readback.columns.tolist(), ['a', 'b'])

    def test_recognize_sourcedata_passed_filelike(self):
        path = fixture_file_path('synthetic-100.csv')
        with open(path, 'rb') as fd:
            kwargs = recognize_sourcedata(fd, self.default_fname)
            self.assertTrue(kwargs.get('filelike') is fd)
            self.assertEqual(kwargs.get('fname'), self.default_fname)

    def test_recognize_sourcedata_passed_filepath(self):
        file_path = fixture_file_path('synthetic-100.csv')
        kwargs = recognize_sourcedata(file_path, self.default_fname)
        self.assertEqual(kwargs.get('file_path'), file_path)
        self.assertEqual(kwargs.get('fname'), 'synthetic-100.csv')

    def test_recognize_sourcedata_passed_content(self):
        content = b'a,b,c\n' + b'1,2,3\n' * 100
        kwargs = recognize_sourcedata(content, self.default_fname)
        self.assertEqual(kwargs.get('content'), content)
        self.assertEqual(kwargs.get('fname'), self.default_fname)

    def test_is_urlsource_passed_true(self):
        result = is_urlsource('http://path_to_urlsource')
        self.assertTrue(result)

    def test_is_urlsource_file_passed_true(self):
        result = is_urlsource('file://path_to_urlsource')
        self.assertTrue(result)

    def test_is_urlsource_file_passed_false(self):
        result = is_urlsource('filename')
        self.assertFalse(result)

    def test_is_urlsource_passed_false(self):
        result = is_urlsource('not_a_path_to_urlsource')
        self.assertFalse(result)

    def test_is_urlsource_s3_passed_true(self):
        result = is_urlsource('s3://path/to/a/bucket/data.csv')
        self.assertTrue(result)

    def test_fatfingered_filepath_raises(self):
        content = b'/home/datarobot/mistypefilepath.csv'
        with self.assertRaises(errors.InputNotUnderstoodError):
            recognize_sourcedata(content, self.default_fname)


def test_resource():
    """Python 3 ResourceWarning of implicitly closed files."""
    with warns():
        open(__file__)


class TestImports(unittest.TestCase):
    def test_rename(self):
        """datarobot_sdk rename."""
        with self.assertRaises(ImportError):
            import datarobot_sdk  # noqa
