import datetime
from unittest import TestCase

from requests_mock import Mocker

from postgrestutils.client import (
    Count, MultipleObjectsReturned, ObjectDoesNotExist,
)
from postgrestutils.client.postgrestclient import LazyPostgrestJsonResult

TOKEN = 'JWT_token'
DEFAULT_HEADERS = {
    'Authorization': 'Bearer {}'.format(TOKEN),
    'Accept': 'application/json'
}
SUPERHERO_TEST_DATA = [
    {
        'id': 68,
        'name': 'Batman',
        'gender': 'Male',
        'eye_color': 'blue',
        'race': 'Human',
        'hair_color': 'black',
        'height': 188,
        'publisher': 'DC Comics',
        'skin_color': None,
        'alignment': 'good',
        'weight': 95
    }, {
        'id': 212,
        'name': 'Deadpool',
        'gender': 'Male',
        'eye_color': 'brown',
        'race': 'Mutant',
        'hair_color': 'No Hair',
        'height': 188,
        'publisher': 'Marvel Comics',
        'skin_color': None,
        'alignment': 'neutral',
        'weight': 95
    }, {
        'id': 345,
        'name': 'Iron Man',
        'gender': 'Male',
        'eye_color': 'blue',
        'race': 'Human',
        'hair_color': 'Black',
        'height': 198,
        'publisher': 'Marvel Comics',
        'skin_color': None,
        'alignment': 'good',
        'weight': 191
    }, {
        'id': 369,
        'name': 'Joker',
        'gender': 'Male',
        'eye_color': 'green',
        'race': 'Human',
        'hair_color': 'Green',
        'height': 196,
        'publisher': 'DC Comics',
        'skin_color': 'white',
        'alignment': 'bad',
        'weight': 86
    }, {
        'id': 423,
        'name': 'Magneto',
        'gender': 'Male',
        'eye_color': 'grey',
        'race': 'Mutant',
        'hair_color': 'White',
        'height': 188,
        'publisher': 'Marvel Comics',
        'skin_color': None,
        'alignment': 'bad',
        'weight': 86
    }
]


@Mocker()
class TestPgrestClientGet(TestCase):
    def setUp(self):
        super().setUp()
        from postgrestutils.client import pgrest_client
        pgrest_client.configure(TOKEN, base_uri='http://example.com/')

        self.pgrest_client = pgrest_client
        self.data = SUPERHERO_TEST_DATA[0]

    def test_single_object_returned(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero?id=eq.1000000000',
            request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}},
            status_code=200,
            reason='OK',
            json=self.data
        )
        params = {'id': 'eq.1000000000'}
        res = self.pgrest_client.get('superhero', params=params)

        self.assertDictEqual(res, self.data)
        self.assertTrue(mock.called_once)

    def test_object_does_not_exist(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero?id=eq.1337',
            request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}},
            status_code=406,
            reason='Not Acceptable',
            text="""{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"""
                 """"JSON object requested, multiple (or no) rows returned"}"""
        )
        params = {'id': 'eq.1337'}

        with self.assertRaises(ObjectDoesNotExist):
            self.pgrest_client.get('superhero', params=params)
        self.assertTrue(mock.called_once)

    def test_multiple_objects_returned(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}},
            status_code=406,
            reason='Not Acceptable',
            text="""{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"""
                 """"JSON object requested, multiple (or no) rows returned"}"""
        )

        with self.assertRaises(MultipleObjectsReturned):
            self.pgrest_client.get('superhero')
        self.assertTrue(mock.called_once)

    def test_datetime_parser(self, mock):
        expected = {
            'id': 1337,
            'random': datetime.datetime(2020, 5, 20, 8, 35, 6, 659425, tzinfo=datetime.timezone.utc)
        }
        mock.register_uri(
            'GET',
            'http://example.com/random_datetime',
            request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}},
            status_code=200,
            reason='OK',
            json={'id': 1337, 'random': "2020-05-20T08:35:06.659425+00:00"}
        )
        params = {'id': 'eq.1337'}
        res = self.pgrest_client.get('random_datetime', params=params)

        self.assertDictEqual(res, expected)
        self.assertTrue(mock.called_once)

    def test_without_datetime_parser(self, mock):
        test_json = {'id': 1337, 'random': "2020-05-20T08:35:06.659425+00:00"}
        mock.register_uri(
            'GET',
            'http://example.com/random_datetime',
            request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}},
            status_code=200,
            reason='OK',
            json=test_json
        )
        params = {'select': 'id,random', 'id': 'eq.1337'}
        res = self.pgrest_client.get('random_datetime', params=params, parse_dt=False)

        self.assertDictEqual(res, test_json)
        self.assertTrue(mock.called_once)


@Mocker()
class TestPgrestClientFilterStrategyNone(TestCase):
    def setUp(self):
        super().setUp()
        from postgrestutils.client import pgrest_client
        pgrest_client.configure(TOKEN, base_uri='http://example.com/')

        self.pgrest_client = pgrest_client
        self.data = SUPERHERO_TEST_DATA

    def test_fetch_all_first(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers=DEFAULT_HEADERS,
            status_code=200,
            reason='OK',
            json=self.data
        )
        res = self.pgrest_client.filter('superhero')

        self.assertIsInstance(res, LazyPostgrestJsonResult)  # should return lazy object
        self.assertFalse(mock.called)  # no request should have been made yet

        self.assertListEqual(list(res), self.data)  # fetch data
        self.assertTrue(mock.called_once)  # should have been called once
        self.assertListEqual(res._result_cache, self.data)  # fetched data should be cached
        self.assertEqual(res._len_cache, len(self.data))  # len of fetched data should be cached
        self.assertListEqual(list(res), self.data)  # should utilize cache
        self.assertListEqual(res[2:], self.data[2:])  # should utilize cache
        self.assertDictEqual(res[0], self.data[0])  # should utilize cache
        self.assertTrue(mock.called_once)  # should not have been called again

    def test_fetch_len_first(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers=DEFAULT_HEADERS,
            status_code=200,
            reason='OK',
            json=self.data
        )
        res = self.pgrest_client.filter('superhero')

        self.assertIsInstance(res, LazyPostgrestJsonResult)  # should return lazy object
        self.assertFalse(mock.called)  # no request should have been made yet

        self.assertEqual(len(res), len(self.data))  # should fetch len
        self.assertTrue(mock.called_once)  # should have been called once
        self.assertEqual(res._len_cache, len(self.data))  # len of fetched data should be cached
        self.assertListEqual(res._result_cache, self.data)  # results should be cached (counting strategy none)
        self.assertListEqual(res[2:], self.data[2:])  # should utilize cache
        self.assertDictEqual(res[0], self.data[0])  # should utilize cache
        self.assertListEqual(list(res), self.data)  # should utilize cache
        self.assertTrue(mock.called_once)  # should not have been called again

    def test_cache_fetching_unbounded_slice(self, mock):
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers=DEFAULT_HEADERS,
            status_code=200,
            reason='OK',
            json=self.data
        )
        res = self.pgrest_client.filter('superhero')

        self.assertIsInstance(res, LazyPostgrestJsonResult)  # should return lazy object
        self.assertFalse(mock.called)  # no request should have been made yet

        self.assertListEqual(res[:], self.data)  # fetch data
        self.assertTrue(mock.called_once)  # should have been called once
        self.assertListEqual(res._result_cache, self.data)  # fetched data should be cached
        self.assertEqual(res._len_cache, len(self.data))  # len of fetched data should be cached
        self.assertListEqual(res[:], self.data)  # should utilize cache
        self.assertListEqual(res[2:], self.data[2:])  # should utilize cache
        self.assertDictEqual(res[0], self.data[0])  # should utilize cache
        self.assertTrue(mock.called_once)  # should not have been called again


@Mocker()
class TestPgrestClientFilterStrategyExact(TestCase):
    def setUp(self):
        super().setUp()
        from postgrestutils.client import pgrest_client
        pgrest_client.configure(TOKEN, base_uri='http://example.com/')

        self.pgrest_client = pgrest_client
        self.data = SUPERHERO_TEST_DATA

    def test_fetch_all_first(self, mock):
        # in order to fetch all
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers=DEFAULT_HEADERS,
            status_code=200,
            reason='OK',
            json=self.data
        )
        res = self.pgrest_client.filter('superhero', count=Count.EXACT)

        self.assertIsInstance(res, LazyPostgrestJsonResult)  # should return lazy object
        self.assertFalse(mock.called)  # no request should have been made yet

        self.assertListEqual(list(res), self.data)  # fetch data
        self.assertTrue(mock.called_once)  # should have been called once
        self.assertListEqual(res._result_cache, self.data)  # fetched data should be cached
        self.assertEqual(res._len_cache, len(self.data))  # len of fetched data should also be cached
        self.assertListEqual(list(res), self.data)  # should utilize cache
        self.assertListEqual(res[2:], self.data[2:])  # should utilize cache
        self.assertDictEqual(res[0], self.data[0])  # should utilize cache
        self.assertTrue(mock.called_once)  # should not have been called again

    def test_fetch_len_first(self, mock):
        # in order to fetch all
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers=DEFAULT_HEADERS,
            status_code=200,
            reason='OK',
            json=self.data
        )
        # in order to fetch first
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '0-0'}},
            status_code=200,
            reason='OK',
            headers={'Content-Range': '0-0/*'},
            json=self.data[0]
        )
        # in order to fetch range since index 2
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '2-'}},
            status_code=200,
            reason='OK',
            headers={'Content-Range': '2-4/*'},
            json=self.data[2:]
        )
        # in order to fetch length
        mock.register_uri(
            'GET',
            'http://example.com/superhero',
            request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '0-0', 'Prefer': 'count=exact'}},
            status_code=206,
            reason='Partial Content',
            headers={'Content-Range': '0-0/5'},
            json=self.data[0]
        )
        res = self.pgrest_client.filter('superhero', count=Count.EXACT)

        self.assertIsInstance(res, LazyPostgrestJsonResult)  # should return lazy object
        self.assertFalse(mock.called)  # no request should have been made yet

        self.assertEqual(len(res), len(self.data))  # should fetch len
        self.assertTrue(mock.called_once)  # should have been called once
        self.assertEqual(res._len_cache, len(self.data))  # len of fetched data should be cached
        self.assertListEqual(res[2:], self.data[2:])  # should fetch range starting at index 2
        self.assertDictEqual(res[0], self.data[0])  # should fetch first element as range
        self.assertListEqual(list(res), self.data)  # should fetch all elements
        self.assertListEqual(res._result_cache, self.data)  # should cache all elements
        self.assertTrue(mock.called)  # should have been called at least once
        self.assertEqual(mock.call_count, 4)  # should have only been called 4 times (fetch len, range, first and all)