from functools import wraps
import json

import pytest

from falcon.errors import HTTPNotFound
import falcon
from falcon.testing import TestBase

from graceful.serializers import BaseSerializer
from graceful.fields import RawField, IntField
from graceful.validators import min_validator
from graceful.resources.generic import (
    RetrieveAPI,
    RetrieveUpdateAPI,
    RetrieveUpdateDeleteAPI,
    ListAPI,
    ListCreateAPI,
    PaginatedListAPI,
    PaginatedListCreateAPI,
)


def index_error_as_404(fun):
    """
    Helper decorator that treats all IndexErrors as HTTP 404 Not Found

    Args:
        fun: function/method to wrap

    """
    @wraps(fun)
    def resource_handler(*args, **kwargs):
        try:
            return fun(*args, **kwargs)
        except IndexError:
            raise HTTPNotFound

    return resource_handler


class ExampleSerializer(BaseSerializer):
    writable = RawField("testing writable field")
    readonly = RawField("testing readonly field", read_only=True)
    nullable = RawField("testing nullable field", allow_null=True)
    unsigned = IntField(
        "testing validated field",
        validators=[min_validator(0)]
    )


class StoredResource:
    def __init__(self, storage=None):
        self.storage = storage or []


class ExampleRetrieveAPI(RetrieveAPI, StoredResource):
    serializer = ExampleSerializer()

    @index_error_as_404
    def retrieve(self, params, meta, index, **kwargs):
        return self.storage[int(index)]


class ExampleRetrieveUpdateAPI(RetrieveUpdateAPI, StoredResource):
    serializer = ExampleSerializer()

    @index_error_as_404
    def retrieve(self, params, meta, index, **kwargs):
        return self.storage[int(index)]

    @index_error_as_404
    def update(self, params, meta, index, validated, **kwargs):
        self.storage[int(index)].update(validated)
        return validated


class ExampleRetrieveUpdateDeleteAPI(RetrieveUpdateDeleteAPI, StoredResource):
    serializer = ExampleSerializer()

    @index_error_as_404
    def retrieve(self, params, meta, index, **kwargs):
        return self.storage[int(index)]

    @index_error_as_404
    def update(self, params, meta, index, validated, **kwargs):
        self.storage[int(index)].update(validated)
        return validated

    @index_error_as_404
    def delete(self, params, meta, index, **kwargs):
        self.storage.pop(int(index))


class ExampleListAPI(ListAPI, StoredResource):
    serializer = ExampleSerializer()

    def list(self, params, meta, **kwargs):
        return self.storage


class ExampleListWithUriVariableAPI(ListAPI, StoredResource):
    serializer = ExampleSerializer()

    def list(self, params, meta, uri_template_variable, **kwargs):
        return self.storage


class ExampleListCreateAPI(ListCreateAPI, StoredResource):
    serializer = ExampleSerializer()

    def list(self, params, meta, **kwargs):
        return self.storage

    def create(self, params, meta, validated, **kwargs):
        self.storage.append(validated)
        return validated


class ExamplePaginatedListAPI(PaginatedListAPI, StoredResource):
    serializer = ExampleSerializer()

    def list(self, params, meta, **kwargs):
        start = params['page_size'] * (params['page'])
        end = params['page_size'] * (params['page'] + 1)
        return self.storage[start:end]


class ExamplePaginatedListCreateAPI(PaginatedListCreateAPI, StoredResource):
    serializer = ExampleSerializer()

    def list(self, params, meta, **kwargs):
        start = params['page_size'] * (params['page'])
        end = params['page_size'] * (params['page'] + 1)
        return self.storage[start:end]

    def create(self, params, meta, validated, **kwargs):
        self.storage.append(validated)
        return validated


def test_implementation_hook_update():
    with pytest.raises(NotImplementedError):
        RetrieveUpdateDeleteAPI().update(None, None)


def test_implementation_hook_retrieve():
    with pytest.raises(NotImplementedError):
        RetrieveUpdateDeleteAPI().retrieve(None, None)


def test_implementation_hook_delete():
    with pytest.raises(NotImplementedError):
        RetrieveUpdateDeleteAPI().delete(None, None)


def test_implementation_hook_list():
    with pytest.raises(NotImplementedError):
        PaginatedListCreateAPI().list(None, None)


def test_implementation_hook_create():
    with pytest.raises(NotImplementedError):
        PaginatedListCreateAPI().create(None, None)


def test_implementation_hook_create_bulk():
    with pytest.raises(NotImplementedError):
        # note: create_bulk() is already implemented but it should
        #       reuse create() that is not implemented
        PaginatedListCreateAPI().create_bulk(None, None, validated=[{}])


class GenericsTestBase(TestBase):
    def setUp(self):
        super(GenericsTestBase, self).setUp()

        self.storage = [{"writeble": "foo", "readonly": "bar"}]

    def _assert_consistent_form(self, result):
        body = json.loads(result)

        assert self.srmock.headers_dict['Content-Type'] == 'application/json'
        assert body
        assert 'meta' in body
        assert 'content' in body


class RetrieveTestsMixin:
    """
    Contains all test that should be performed on Resource that supports
    retrieve
    """
    uri_template = '/items/{index}'

    def test_retrieve(self):
        result = self.simulate_request(
            self.uri_template.format(index=0), decode='utf-8'
        )
        self._assert_consistent_form(result)

        assert self.srmock.status == falcon.HTTP_OK

    def test_retrieve_not_found(self):
        # note: only one item in storage so this will return 404
        self.simulate_request(
            self.uri_template.format(index=1), decode='utf-8'
        )
        assert self.srmock.status == falcon.HTTP_NOT_FOUND

    def test_options(self):
        result = self.simulate_request(
            self.uri_template.format(index=1), decode='utf-8', method='OPTIONS'
        )
        description = json.loads(result)
        assert description['type'] == 'object'
        assert 'fields' in description


class UpdateTestsMixin:
    """
    Contains all test that should be performed on Resource that supports
    update
    """
    uri_template = '/items/{index}'

    def do_update(self, index_, representation):
        return self.simulate_request(
            self.uri_template.format(index=index_),
            decode='utf-8',
            method='PUT',
            headers={'Content-Type': 'application/json'},
            body=json.dumps(representation),
        )

    def test_update(self):
        result = self.do_update(0, {'writable': 'changed', 'unsigned': 12,
                                    'nullable': None})
        self._assert_consistent_form(result)
        assert self.srmock.status == falcon.HTTP_ACCEPTED

        body = json.loads(result)
        assert body['content']['writable'] == 'changed'

    def test_update_not_found(self):
        self.do_update(1, {'writable': 'changed', 'unsigned': 12,
                           'nullable': None})
        assert self.srmock.status == falcon.HTTP_NOT_FOUND

    def test_update_readonly_field_error(self):
        self.do_update(
            0, {'writable': 'changed', 'unsigned': 12, 'readonly': 'changed'}
        )
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_update_missing_field_error(self):
        self.do_update(0, {'writable': 'changed'})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_update_parse_error(self):
        # note: 'unsigned' that can't be parsed
        self.do_update(0, {'writable': 'changed', 'unsigned': 'foo'})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_update_validation_error(self):
        # note: 'unsigned' that is < 0
        self.do_update(0, {'writable': 'changed', 'unsigned': -12})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_update_unsuported_media_type(self):
        self.simulate_request(
            self.uri_template.format(index=0),
            decode='utf-8',
            method='PUT',
            headers={'Content-Type': 'unsupported'},
            body="foo bar",
        )
        assert self.srmock.status == falcon.HTTP_UNSUPPORTED_MEDIA_TYPE


class DeleteTestsMixin:
    """
    Contains all test that should be performed on resource that supports
    delete
    """
    uri_template = '/items/{index}'

    def do_delete(self, index_):
        return self.simulate_request(
            self.uri_template.format(index=index_),
            decode='utf-8',
            method='DELETE',
        )

    def test_delete(self):
        response = self.do_delete(0)
        self._assert_consistent_form(response)
        assert self.storage == []

    def test_delete_not_found(self):
        self.do_delete(1)
        assert self.srmock.status == falcon.HTTP_NOT_FOUND


class ListTestsMixin:
    """
    Contains all tests that should be performed on resource that supports list
    """
    uri_template = '/items/'

    def test_list(self):
        result = self.simulate_request(self.uri_template, decode='utf-8')
        self._assert_consistent_form(result)

        assert self.srmock.status == falcon.HTTP_OK

    def test_options(self):
        result = self.simulate_request(
            self.uri_template, decode='utf-8', method='OPTIONS'
        )
        description = json.loads(result)
        assert description['type'] == 'list'
        assert 'fields' in description


class PaginationTestsMixin:
    uri_template = '/items/'

    def test_list_pagination(self):
        for _ in range(100):
            self.storage.append({"writeble": "foo", "readonly": "bar"})

        result = self.simulate_request(self.uri_template, decode='utf-8')
        body = json.loads(result)

        assert 'next' in body['meta']
        assert 'prev' in body['meta']
        assert len(body['content']) == body['meta']['page_size']

        # now try to access page without results
        result = self.simulate_request(
            self.uri_template, decode='utf-8', query_string="page=1000"
        )
        body = json.loads(result)
        assert len(body['content']) == 0


class CreateTestsMixin:
    """
    Contains all tests that should be performed on resource that suports create
    """
    uri_template = '/items/'

    def do_create(self, representation):
        return self.simulate_request(
            self.uri_template,
            decode='utf-8',
            method='POST',
            headers={'Content-Type': 'application/json'},
            body=json.dumps(representation),
        )

    def do_create_bulk(self, representation):
        return self.simulate_request(
            self.uri_template,
            decode='utf-8',
            method='PATCH',
            headers={'Content-Type': 'application/json'},
            body=json.dumps(representation),
        )

    def test_create(self):
        result = self.do_create(
            {'writable': 'changed', 'unsigned': 12, 'nullable': None}
        )
        self._assert_consistent_form(result)

        assert self.srmock.status == falcon.HTTP_CREATED

    def test_create_readonly_field_error(self):
        self.do_create(
            {'writable': 'changed', 'unsigned': 12, 'readonly': 'changed'}
        )
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_create_missing_field_error(self):
        self.do_create({'writable': 'changed'})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_create_parse_error(self):
        # note: 'unsigned' that can't be parsed
        self.do_create({'writable': 'changed', 'unsigned': 'foo'})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_create_validation_error(self):
        # note: 'unsigned' that is < 0
        self.do_create({'writable': 'changed', 'unsigned': -12})
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST

    def test_create_unsuported_media_type(self):
        self.simulate_request(
            self.uri_template,
            decode='utf-8',
            method='POST',
            headers={'Content-Type': 'unsupported'},
            body="foo bar",
        )
        assert self.srmock.status == falcon.HTTP_UNSUPPORTED_MEDIA_TYPE

    def test_create_bulk(self):
        self.do_create_bulk(
            [{'writable': 'changed', 'unsigned': 12, 'nullable': None}]
        )
        assert self.srmock.status == falcon.HTTP_CREATED

    def test_create_bulk_without_list_results_in_bad_request(self):
        self.do_create_bulk(
            {'writable': 'changed', 'unsigned': 12}
        )
        assert self.srmock.status == falcon.HTTP_BAD_REQUEST


# actual test case classes here

class RetrieveTestCase(
    RetrieveTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(RetrieveTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleRetrieveAPI(self.storage)
        )


class RetrieveUpdateTestCase(
    RetrieveTestsMixin,
    UpdateTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(RetrieveUpdateTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleRetrieveUpdateAPI(self.storage)
        )


class RetrieveUpdateDeleteTestCase(
    RetrieveTestsMixin,
    UpdateTestsMixin,
    DeleteTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(RetrieveUpdateDeleteTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleRetrieveUpdateDeleteAPI(self.storage)
        )


class ListTestCase(
    ListTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(ListTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleListAPI(self.storage)
        )


class ListWithUriVariableTestCase(
    ListTestsMixin,
    GenericsTestBase,
):
    """ Tests that ListAPI-based endpoints access URI template variables
    """
    uri_template = '/items/{uri_template_variable}'

    def setUp(self):
        super(ListWithUriVariableTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleListWithUriVariableAPI(self.storage),
        )


class ListCreateTestCase(
    ListTestsMixin,
    CreateTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(ListCreateTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExampleListCreateAPI(self.storage)
        )


class PaginatedListTestCase(
    ListTestsMixin,
    PaginationTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(PaginatedListTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExamplePaginatedListAPI(self.storage)
        )


class PaginatedListCreateTestCase(
    ListTestsMixin,
    CreateTestsMixin,
    PaginationTestsMixin,
    GenericsTestBase,
):
    def setUp(self):
        super(PaginatedListCreateTestCase, self).setUp()
        self.api.add_route(
            self.uri_template,
            ExamplePaginatedListCreateAPI(self.storage)
        )