# test_manager.py - unit tests for the manager module
#
# Copyright 2011 Lincoln de Sousa <lincoln@comum.org>.
# Copyright 2012, 2013, 2014, 2015, 2016 Jeffrey Finkelstein
#           <jeffrey.finkelstein@gmail.com> and contributors.
#
# This file is part of Flask-Restless.
#
# Flask-Restless is distributed under both the GNU Affero General Public
# License version 3 and under the 3-clause BSD license. For more
# information, see LICENSE.AGPL and LICENSE.BSD.
"""Unit tests for the :mod:`flask_restless.manager` module."""

from flask import Flask
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import Unicode
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship

from flask_restless import APIManager
from flask_restless import IllegalArgumentError
from flask_restless import collection_name
from flask_restless import model_for
from flask_restless import serializer_for
from flask_restless import url_for

from .helpers import FlaskSQLAlchemyTestBase
from .helpers import ManagerTestBase
from .helpers import SQLAlchemyTestBase
from .helpers import force_content_type_jsonapi
from .helpers import loads


class TestLocalAPIManager(SQLAlchemyTestBase):
    """Provides tests for :class:`flask_restless.APIManager` when the tests
    require that the instance of :class:`flask_restless.APIManager` has not
    yet been instantiated.

    """

    def setUp(self):
        super().setUp()

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)

        self.Person = Person
        self.Article = Article
        self.Base.metadata.create_all()

    def test_missing_session(self):
        """Tests that setting neither a session nor a Flask-SQLAlchemy
        object yields an error.

        """
        with self.assertRaises(ValueError):
            APIManager(app=self.flaskapp)

    def test_constructor_app(self):
        """Tests for providing a :class:`~flask.Flask` application in
        the constructor.

        """
        manager = APIManager(app=self.flaskapp, session=self.session)
        manager.create_api(self.Person)
        response = self.app.get('/api/person')
        assert response.status_code == 200

    def test_single_manager_init_single_app(self):
        """Tests for calling :meth:`~APIManager.init_app` with a single
        :class:`~flask.Flask` application after calling
        :meth:`~APIManager.create_api`.

        """
        manager = APIManager(session=self.session)
        manager.create_api(self.Person)
        manager.init_app(self.flaskapp)
        response = self.app.get('/api/person')
        assert response.status_code == 200

    def test_single_manager_init_multiple_apps(self):
        """Tests for calling :meth:`~APIManager.init_app` on multiple
        :class:`~flask.Flask` applications after calling
        :meth:`~APIManager.create_api`.

        """
        manager = APIManager(session=self.session)
        flaskapp1 = self.flaskapp
        flaskapp2 = Flask(__name__)
        testclient1 = self.app
        testclient2 = flaskapp2.test_client()
        force_content_type_jsonapi(testclient2)
        manager.create_api(self.Person)
        manager.init_app(flaskapp1)
        manager.init_app(flaskapp2)
        response = testclient1.get('/api/person')
        assert response.status_code == 200
        response = testclient2.get('/api/person')
        assert response.status_code == 200

    def test_multiple_managers_init_single_app(self):
        """Tests for calling :meth:`~APIManager.init_app` on a single
        :class:`~flask.Flask` application after calling
        :meth:`~APIManager.create_api` on multiple instances of
        :class:`APIManager`.

        """
        manager1 = APIManager(session=self.session)
        manager2 = APIManager(session=self.session)

        # First create the API, then initialize the Flask applications after.
        manager1.create_api(self.Person)
        manager2.create_api(self.Article)
        manager1.init_app(self.flaskapp)
        manager2.init_app(self.flaskapp)

        # Tests that both endpoints are accessible on the Flask application.
        response = self.app.get('/api/person')
        assert response.status_code == 200
        response = self.app.get('/api/article')
        assert response.status_code == 200

    def test_multiple_managers_init_multiple_apps(self):
        """Tests for calling :meth:`~APIManager.init_app` on multiple
        :class:`~flask.Flask` applications after calling
        :meth:`~APIManager.create_api` on multiple instances of
        :class:`APIManager`.

        """
        manager1 = APIManager(session=self.session)
        manager2 = APIManager(session=self.session)

        # Create the Flask applications and the test clients.
        flaskapp1 = self.flaskapp
        flaskapp2 = Flask(__name__)
        testclient1 = self.app
        testclient2 = flaskapp2.test_client()
        force_content_type_jsonapi(testclient2)

        # First create the API, then initialize the Flask applications after.
        manager1.create_api(self.Person)
        manager2.create_api(self.Article)
        manager1.init_app(flaskapp1)
        manager2.init_app(flaskapp2)

        # Tests that only the first Flask application gets requests for
        # /api/person and only the second gets requests for /api/article.
        response = testclient1.get('/api/person')
        assert response.status_code == 200
        response = testclient1.get('/api/article')
        assert response.status_code == 404
        response = testclient2.get('/api/person')
        assert response.status_code == 404
        response = testclient2.get('/api/article')
        assert response.status_code == 200

    def test_universal_preprocessor(self):
        """Tests universal preprocessor and postprocessor applied to all
        methods created with the API manager.

        """
        class Counter:
            """An object that increments a counter on each invocation."""

            def __init__(self):
                self._counter = 0

            def __call__(self, *args, **kw):
                self._counter += 1

            def __eq__(self, other):
                if isinstance(other, Counter):
                    return self._counter == other._counter
                if isinstance(other, int):
                    return self._counter == other
                return False

        increment1 = Counter()
        increment2 = Counter()

        preprocessors = dict(GET_COLLECTION=[increment1])
        postprocessors = dict(GET_COLLECTION=[increment2])
        manager = APIManager(self.flaskapp, session=self.session,
                             preprocessors=preprocessors,
                             postprocessors=postprocessors)
        manager.create_api(self.Person)
        manager.create_api(self.Article)
        # After each request, regardless of API endpoint, both counters should
        # be incremented.
        self.app.get('/api/person')
        self.app.get('/api/article')
        self.app.get('/api/person')
        assert increment1 == increment2 == 3

    def test_url_prefix(self):
        """Tests for specifying a URL prefix at the manager level but
        not when creating an API.

        """
        manager = APIManager(self.flaskapp, session=self.session,
                             url_prefix='/foo')
        manager.create_api(self.Person)
        response = self.app.get('/foo/person')
        assert response.status_code == 200
        response = self.app.get('/api/person')
        assert response.status_code == 404

    def test_empty_url_prefix(self):
        """Tests for specifying an empty string as URL prefix at the manager
        level but not when creating an API.

        """
        manager = APIManager(self.flaskapp, session=self.session,
                             url_prefix='')
        manager.create_api(self.Person)
        response = self.app.get('/person')
        assert response.status_code == 200
        response = self.app.get('/api/person')
        assert response.status_code == 404

    def test_override_url_prefix(self):
        """Tests that a call to :meth:`APIManager.create_api` can
        override the URL prefix provided in the constructor to the
        manager class, if the new URL starts with a slash.

        """
        manager = APIManager(self.flaskapp, session=self.session,
                             url_prefix='/foo')
        manager.create_api(self.Person, url_prefix='/bar')
        manager.create_api(self.Article, url_prefix='')
        response = self.app.get('/bar/person')
        assert response.status_code == 200
        response = self.app.get('/article')
        assert response.status_code == 200
        response = self.app.get('/foo/person')
        assert response.status_code == 404
        response = self.app.get('/foo/article')
        assert response.status_code == 404

    # # This is a possible feature, but we will not support this for now.
    # def test_append_url_prefix(self):
    #     """Tests that a call to :meth:`APIManager.create_api` can
    #     append to the URL prefix provided in the constructor to the
    #     manager class, if the new URL does not start with a slash.

    #     """
    #     manager = APIManager(self.flaskapp, session=self.session,
    #                          url_prefix='/foo')
    #     manager.create_api(self.Person, url_prefix='bar')
    #     response = self.app.get('/foo/bar/person')
    #     assert response.status_code == 200
    #     response = self.app.get('/foo/person')
    #     assert response.status_code == 404
    #     response = self.app.get('/bar/person')
    #     assert response.status_code == 404


class TestAPIManager(ManagerTestBase):
    """Unit tests for the :class:`flask_restless.manager.APIManager` class."""

    def setUp(self):
        super().setUp()

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)
            name = Column(Unicode)

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            title = Column(Unicode)
            author_id = Column(Integer, ForeignKey('person.id'))
            author = relationship(Person, backref=backref('articles'))

        class Tag(self.Base):
            __tablename__ = 'tag'
            name = Column(Unicode, primary_key=True)

        self.Article = Article
        self.Person = Person
        self.Tag = Tag
        self.Base.metadata.create_all()

    # HACK If we don't include this, there seems to be an issue with the
    # globally known APIManager objects not being cleared after every test.
    def tearDown(self):
        """Clear the :class:`flask_restless.APIManager` objects known by
        the global functions :data:`model_for`, :data:`url_for`, and
        :data:`collection_name`.

        """
        super(TestAPIManager, self).tearDown()
        model_for.created_managers.clear()
        url_for.created_managers.clear()
        collection_name.created_managers.clear()
        serializer_for.created_managers.clear()
        model_for.__call__.cache_clear()

    def test_url_for(self):
        """Tests the global :func:`flask_restless.url_for` function."""
        self.manager.create_api(self.Person, collection_name='people')
        self.manager.create_api(self.Article, collection_name='articles')
        with self.flaskapp.test_request_context():
            url1 = url_for(self.Person)
            url2 = url_for(self.Person, resource_id=1)
            url3 = url_for(self.Person, resource_id=1,
                           relation_name='articles')
            url4 = url_for(self.Person, resource_id=1,
                           relation_name='articles', related_resource_id=2)
            assert url1.endswith('/api/people')
            assert url2.endswith('/api/people/1')
            assert url3.endswith('/api/people/1/articles')
            assert url4.endswith('/api/people/1/articles/2')

    def test_url_for_explicitly_sets_primary_key_in_links(self):
        """Should use the primary_key explicitly set when generating links"""
        article = self.Article(id=1, title=u'my_article')
        self.session.add(article)
        self.session.commit()
        self.manager.create_api(self.Article, primary_key='title')

        response = self.app.get('/api/article')
        document = loads(response.data)
        articles = document['data']
        article = articles[0]

        assert 'my_article' in article['links']['self']
        assert '/1' not in article['links']['self']
        author_links = article['relationships']['author']['links']
        assert author_links['self'] == (
            '/api/article/my_article/relationships/author')

    def test_url_for_nonexistent(self):
        """Tests that attempting to get the URL for an unknown model yields an
        error.

        """
        with self.assertRaises(ValueError):
            url_for(self.Person)

    def test_collection_name(self):
        """Tests the global :func:`flask_restless.collection_name`
        function.

        """
        self.manager.create_api(self.Person, collection_name='people')
        assert collection_name(self.Person) == 'people'

    def test_collection_name_nonexistent(self):
        """Tests that attempting to get the collection name for an unknown
        model yields an error.

        """
        with self.assertRaises(ValueError):
            collection_name(self.Person)

    def test_serializer_for(self):
        """Tests the global :func:`flask_restless.serializer_for`
        function.

        """
        def my_function(*args, **kw):
            pass

        self.manager.create_api(self.Person, serializer=my_function)
        assert serializer_for(self.Person) == my_function

    def test_serializer_for_nonexistent(self):
        """Tests that attempting to get the serializer for an unknown
        model yields an error.

        """
        with self.assertRaises(ValueError):
            serializer_for(self.Person)

    def test_model_for(self):
        """Tests the global :func:`flask_restless.model_for` function."""
        self.manager.create_api(self.Person, collection_name='people')
        assert model_for('people') is self.Person

    def test_model_for_nonexistent(self):
        """Tests that attempting to get the model for a nonexistent collection
        yields an error.

        """
        with self.assertRaises(ValueError):
            model_for('people')

    def test_model_for_collection_name(self):
        """Tests that :func:`flask_restless.model_for` is the inverse of
        :func:`flask_restless.collection_name`.

        """
        self.manager.create_api(self.Person, collection_name='people')
        assert collection_name(model_for('people')) == 'people'
        assert model_for(collection_name(self.Person)) is self.Person

    def test_disallowed_methods(self):
        """Tests that disallowed methods respond with :http:status:`405`."""
        self.manager.create_api(self.Person, methods=[])
        for method in 'get', 'post', 'patch', 'delete':
            func = getattr(self.app, method)
            response = func('/api/person')
            assert response.status_code == 405

    def test_missing_id(self):
        """Tests that calling :meth:`APIManager.create_api` on a model without
        an ``id`` column raises an exception.

        """
        with self.assertRaises(IllegalArgumentError):
            self.manager.create_api(self.Tag)

    def test_empty_collection_name(self):
        """Tests that calling :meth:`APIManager.create_api` with an empty
        collection name raises an exception.

        """
        with self.assertRaises(IllegalArgumentError):
            self.manager.create_api(self.Person, collection_name='')

    def test_disallow_functions(self):
        """Tests that if the ``allow_functions`` keyword argument is ``False``,
        no endpoint will be made available at :http:get:`/api/eval/:type`.

        """
        self.manager.create_api(self.Person, allow_functions=False)
        response = self.app.get('/api/eval/person')
        assert response.status_code == 404

    def test_only_and_exclude(self):
        """Tests that attempting to use both ``only`` and ``exclude``
        keyword arguments yields an error.

        """
        with self.assertRaises(IllegalArgumentError):
            self.manager.create_api(self.Person, only=['id'], exclude=['name'])

    def test_additional_attributes_nonexistent(self):
        """Tests that an attempt to include an additional attribute that
        does not exist on the model raises an exception at the time of
        API creation.

        """
        with self.assertRaises(AttributeError):
            self.manager.create_api(self.Person, additional_attributes=['bogus'])


class TestFSA(FlaskSQLAlchemyTestBase):
    """Tests which use models defined using Flask-SQLAlchemy instead of pure
    SQLAlchemy.

    """

    def setUp(self):
        """Creates the Flask application, the APIManager, the database, and the
        Flask-SQLAlchemy models.

        """
        super().setUp()

        class Person(self.db.Model):
            id = self.db.Column(self.db.Integer, primary_key=True)

        self.Person = Person
        self.db.create_all()

    def test_init_app(self):
        manager = APIManager(flask_sqlalchemy_db=self.db)
        manager.create_api(self.Person)
        manager.init_app(self.flaskapp)
        response = self.app.get('/api/person')
        assert response.status_code == 200
