#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    A py.test plugin which helps testing Flask applications.

    :copyright: (c) by Vital Kudzelka
    :license: MIT
"""
import sys

import pytest

from flask import json
from werkzeug.utils import cached_property

from .fixtures import (
    client, config, accept_json, accept_jsonp, accept_any, accept_mimetype,
    client_class, live_server, request_ctx
)
from .pytest_compat import getfixturevalue


class JSONResponse(object):
    """Mixin with testing helper methods for JSON responses."""

    @cached_property
    def json(self):
        """Try to deserialize response data (a string containing a valid JSON
        document) to a Python object by passing it to the underlying
        :mod:`flask.json` module.
        """
        return json.loads(self.data)

    def __eq__(self, other):
        if isinstance(other, int):
            return self.status_code == other
        # even though the Python 2-specific code works on Python 3, keep the two versions
        # separate so we can simplify the code once Python 2 support is dropped
        if sys.version_info[0] == 2:
            try:
                super_eq = super(JSONResponse, self).__eq__
            except AttributeError:
                return NotImplemented
            else:
                return super_eq(other)
        else:
            return super(JSONResponse, self).__eq__(other)

    def __ne__(self, other):
        return not self == other


def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, JSONResponse) and op == '==' and isinstance(right, int):
        return [
            'Mismatch in status code for response: {} != {}'.format(
                left.status_code,
                right,
            ),
            'Response status: {}'.format(left.status),
        ]
    return None


def _make_test_response_class(response_class):
    """Extends the response class with special attribute to test JSON
    responses. Don't override user-defined `json` attribute if any.

    :param response_class: An original response class.
    """
    if 'json' in response_class.__dict__:
        return response_class

    return type(str(JSONResponse), (response_class, JSONResponse), {})


@pytest.fixture(autouse=True)
def _monkeypatch_response_class(request, monkeypatch):
    """Set custom response class before test suite and restore the original
    after. Custom response has `json` property to easily test JSON responses::

        @app.route('/ping')
        def ping():
            return jsonify(ping='pong')

        def test_json(client):
            res = client.get(url_for('ping'))
            assert res.json == {'ping': 'pong'}

    """
    if 'app' not in request.fixturenames:
        return

    app = getfixturevalue(request, 'app')
    monkeypatch.setattr(app, 'response_class',
                        _make_test_response_class(app.response_class))


@pytest.fixture(autouse=True)
def _push_request_context(request):
    """During tests execution request context has been pushed, e.g. `url_for`,
    `session`, etc. can be used in tests as is::

        def test_app(app, client):
            assert client.get(url_for('myview')).status_code == 200

    """
    if 'app' not in request.fixturenames:
        return

    app = getfixturevalue(request, 'app')

    # Get application bound to the live server if ``live_server`` fixture
    # is applied. Live server application has an explicit ``SERVER_NAME``,
    # so ``url_for`` function generates a complete URL for endpoint which
    # includes application port as well.
    if 'live_server' in request.fixturenames:
        app = getfixturevalue(request, 'live_server').app

    ctx = app.test_request_context()
    ctx.push()

    def teardown():
        ctx.pop()

    request.addfinalizer(teardown)


@pytest.fixture(autouse=True)
def _configure_application(request, monkeypatch):
    """Use `pytest.mark.options` decorator to pass options to your application
    factory::

        @pytest.mark.options(debug=False)
        def test_something(app):
            assert not app.debug, 'the application works not in debug mode!'

    """
    if 'app' not in request.fixturenames:
        return

    app = getfixturevalue(request, 'app')
    for options in request.node.iter_markers('options'):
        for key, value in options.kwargs.items():
            monkeypatch.setitem(app.config, key.upper(), value)


def pytest_addoption(parser):
    group = parser.getgroup('flask')
    group.addoption('--start-live-server',
                    action="store_true", dest="start_live_server", default=True,
                    help="start server automatically when live_server "
                         "fixture is applied (enabled by default).")
    group.addoption('--no-start-live-server',
                    action="store_false", dest="start_live_server",
                    help="don't start server automatically when live_server "
                         "fixture is applied.")
    group.addoption('--live-server-clean-stop',
                    action="store_true", dest="live_server_clean_stop", default=True,
                    help="attempt to kill the live server cleanly.")
    group.addoption('--no-live-server-clean-stop',
                    action="store_false", dest="live_server_clean_stop",
                    help="terminate the server forcefully after stop.")
    group.addoption('--live-server-host', action='store', default='localhost', type=str,
                    help='use a host where to listen (default localhost).')
    group.addoption('--live-server-port', action='store', default=0, type=int,
                    help='use a fixed port for the live_server fixture.')


def pytest_configure(config):
    config.addinivalue_line(
        'markers',
        'app(options): pass options to your application factory')
    config.addinivalue_line('markers', 'options: app config manipulation')
