"""
TEST ARTICLES API

Contains fixtures, unit tests, and integration tests for 'api/controllers/articles.py'
"""

import pytest
import datetime

from api.controllers.articles import parse_errors

from api.models import Article, article_schema

"""
Use these fixtures for every test in this module:
client
database
---
It will be assumed for unit and integration tests that the application is running and that a database exists. If these conditions are false then there are problems beyond the scope of unit testing.
"""

pytestmark = pytest.mark.usefixtures("client", "database")


##################
#### FIXTURES ####
##################


@pytest.fixture
def mock_articles(database):
    db = database

    article_1, errors = article_schema.load(
        dict(title="Test article 1", content="Lorem ipsum 123")
    )
    article_2, errors = article_schema.load(
        dict(title="Test article 2", content="Lorem ipsum 123")
    )
    article_3, errors = article_schema.load(
        dict(title="Test article 3", content="Lorem ipsum 123")
    )

    db.session.add(article_1)
    db.session.add(article_2)
    db.session.add(article_3)

    db.session.commit()

    yield

    db.session.query(Article).delete()
    db.session.commit()


####################
#### UNIT TESTS ####
####################


def test_parse_errors():
    """
    GIVEN a validation error dictionary generated by marshmallow
    WHEN passed to parse_errors
    THEN return a dictionary of formatted error messages
    """
    errors = dict(field=["Error message."])
    actual = parse_errors(errors)
    expected = dict(errors=["field: error message"])
    assert expected == actual


###########################
#### INTEGRATION TESTS ####
#### Or maybe these are still unit tests, since
#### they only integrate a single function
#### with an empty database?
###########################

# GET ARTICLES COLLECTION (ALL ARTICLES)
class TestArticlesGetCollection(object):
    # must specify client fixture as argument
    # because it is used explicitly within the test
    # to do GET request.
    def test_get_empty_articles(self, client):
        """
        GIVEN a database containing no articles
        WHEN a GET request is made to '/articles/'
        THEN return an empty json array
            AND return a 200 status code
        """
        rv = client.get("/articles/")

        actual = rv.get_json()
        expected = []
        assert expected == actual

        actual = rv.status_code
        expected = 200
        assert expected == actual

    def test_get_all_articles(self, client, mock_articles):
        """
        GIVEN a database containing three mock articles
        WHEN a GET request is made to '/articles/'
        THEN return an array of articles
            AND return a 200 status code
        """
        rv = client.get("/articles/")

        actual = rv.get_json()
        assert len(actual) is 3

        actual = rv.get_json()[0]
        expected = dict(
            id=1,
            title="Test article 1",
            slug="test-article-1",
            content="Lorem ipsum 123",
        )
        for key in actual.keys():
            if key == "date_created":
                assert key
            else:
                assert actual[key] == expected[key]

        actual = rv.status_code
        expected = 200
        assert expected == actual


# GET ARTICLE RESOURCE (SINGLE ARTICLE)
class TestArticlesGetResource(object):
    def test_get_article_by_slug(self, client, mock_articles):
        """
        GIVEN a database containing three mock articles
        WHEN a GET request is made to '/articles/<slug>'
        THEN return an object containing the requested article.
            AND return a 200 status code
        """
        rv = client.get("/articles/test-article-1")

        actual = rv.status_code
        expected = 200
        assert expected == actual

        actual = rv.get_json()
        expected = {
            "id": 1,
            "slug": "test-article-1",
            "title": "Test article 1",
            "content": "Lorem ipsum 123",
        }
        for key in actual.keys():
            if key == "date_created":
                assert key
            else:
                assert actual[key] == expected[key]

    def test_get_article_not_found(self, client):
        """
        GIVEN a database
        WHEN a GET request is made for an article that doesn't exist
        THEN return an object containing an error message
            AND return a 404 (not found) status code
        """
        rv = client.get("/articles/test-article-1")

        actual = rv.status_code
        expected = 404
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["article not found"])
        assert expected == actual


# TEST CREATE NEW ARTICLE
class TestArticlesCreateResource(object):
    def test_create_article(self, client):
        """
        GIVEN a database
        WHEN a POST request is made to articles root
        THEN create a new article in the database
            AND return that article as a json object
            AND return a 201(created) status code
        """
        new_article = dict(title="Test Article", content="Lorem ipsum 123")

        rv = client.post("/articles/", json=new_article)

        actual = rv.status_code
        expected = 201
        assert expected == actual

        actual = rv.get_json()
        expected = dict(
            title="Test Article", slug="test-article", content="Lorem ipsum 123"
        )
        assert actual["title"] == expected["title"]
        assert actual["slug"] == expected["slug"]
        assert actual["content"] == expected["content"]
        assert actual["date_created"]

    def test_create_article_no_request_body(self, client):
        """
        GIVEN ...
        WHEN a POST is made with no request body
        THEN return an error message
            AND return a 400 status code
        """
        rv = client.post("/articles/")

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["request body is missing or is invalid json"])
        assert expected == actual

    def test_create_article_missing_title(self, client):
        """
        GIVEN ...
        WHEN a POST is made to articles, but the request body is missing a title
        THEN return an error
            AND return a 400 status code
        """
        request_data = dict(content="Lorem ipsum 123")
        rv = client.post("/articles/", json=request_data)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["title: missing data for required field"])
        assert expected == actual

    def test_create_article_missing_content(self, client):
        """
        GIVEN ...
        WHEN a POST is made to articles, but the request body is missing the content of the article
        THEN return an error
            AND return a 400 status code
        """
        request_data = dict(title="Test title")
        rv = client.post("/articles/", json=request_data)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["content: missing data for required field"])
        assert expected == actual

    def test_create_article_unique_title(self, client, mock_articles):
        """
        GIVEN an article exists in the database
        WHEN a POST is made to articles with a title whose text matches that of an existing title, regardless of case
        THEN return an error
            AND return a 400 status code
        """
        request_data = dict(title="TEST ARTICLE 1", content="Lorem ipsum 123")
        rv = client.post("/articles/", json=request_data)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["title: must be unique"])
        assert expected == actual


# TEST UPDATE EXISTING ARTICLE
class TestArticlesUpdateResource(object):
    def test_update_article(self, client, database, mock_articles):
        """
        GIVEN a database containing articles
        WHEN a PUT request is made to an article resource
        THEN update the given article in the database
            AND return the updated article as json object
            AND return a 200 status code
        """
        article_update = dict(title="New Title", content="New article body")

        rv = client.put("/articles/test-article-1", json=article_update)

        actual = rv.status_code
        expected = 200
        assert expected == actual

        # set by server
        article_update["slug"] = "test-article-1"
        # check response contains updated record
        actual = rv.get_json()
        expected = article_update
        for key in expected.keys():
            assert actual[key] == expected[key]

        # check that a new database record was not created
        actual = len(client.get("/articles/").get_json())
        expected = 3
        assert expected == actual

        # test that the database record was actually updated
        actual = client.get("/articles/test-article-1").get_json()
        expected = article_update
        for key in expected.keys():
            assert actual[key] == expected[key]

    def test_update_article_to_existing_title(self, client, database, mock_articles):
        """
        GIVEN a database containing articles
        WHEN a PUT request is made to an article resource containing an updated title that is the same as an existing title
        THEN return an error
            AND return a 400 status code
        """
        article_update = dict(title="Test article 2", content="New article body")

        rv = client.put("/articles/test-article-2", json=article_update)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["title: must be unique"])
        assert expected == actual

    def test_update_article_no_request_body(self, client, mock_articles):
        """
        GIVEN ...
        WHEN a PUT is made to an article with no request body
        THEN return an error message
            AND return a 400 status code
        """
        rv = client.put("/articles/test-article-1")

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["request body is missing or is invalid json"])
        assert expected == actual

    def test_update_article_missing_title(self, client, mock_articles):
        """
        GIVEN ...
        WHEN a PUT is made to articles, but the request body is missing a title
        THEN return an error
            AND return a 400 status code
        """
        request_data = dict(content="Lorem ipsum 123")
        rv = client.put("/articles/test-article-1", json=request_data)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["title: missing data for required field"])
        assert expected == actual

    def test_update_article_missing_content(self, client, mock_articles):
        """
        GIVEN ...
        WHEN a PUT is made to articles, but the request body is missing the content of the article
        THEN return an error
            AND return a 400 status code
        """
        request_data = dict(title="Test article 1")
        rv = client.put("/articles/test-article-1", json=request_data)

        actual = rv.status_code
        expected = 400
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["content: missing data for required field"])
        assert expected == actual


class TestArticlesDeleteResource(object):
    def test_delete_article_by_slug_response(self, client, mock_articles):
        """
        GIVEN a database containing users
        WHEN a DELETE request is made to '/articles/<slug>'
        THEN delete that article from the database
            AND return a 204(no content) status code.
        """
        actual = client.delete("/articles/test-article-1").status_code
        expected = 204
        assert expected == actual

        # test user really gone
        actual = len(client.get("/articles/").get_json())
        expected = 2
        assert expected == actual

        actual = client.get("/articles/test-article-1").status_code
        expected = 404
        assert expected == actual

    def test_delete_article_not_found(self, client):
        """
        GIVEN a database
        WHEN a DELETE request is made for an article that doesn't exist
        THEN return a 404 (not found) error status code
        """
        rv = client.delete("/articles/test-article-99")
        actual = rv.status_code
        expected = 404
        assert expected == actual

        actual = rv.get_json()
        expected = dict(errors=["article not found"])
        assert expected == actual
