from unittest import mock
from urllib.parse import quote_plus

import flask
import pytest
import responses
from oauthlib.oauth1.rfc5849.utils import parse_authorization_header
from werkzeug.middleware.proxy_fix import ProxyFix

from flask_dance.consumer import (
    OAuth1ConsumerBlueprint,
    oauth_authorized,
    oauth_before_login,
    oauth_error,
)
from flask_dance.consumer.requests import OAuth1Session
from flask_dance.consumer.storage import MemoryStorage

try:
    import blinker
except ImportError:
    blinker = None
requires_blinker = pytest.mark.skipif(not blinker, reason="requires blinker")


def make_app(login_url=None):
    blueprint = OAuth1ConsumerBlueprint(
        "test-service",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_to="index",
        login_url=login_url,
    )
    app = flask.Flask(__name__)
    app.secret_key = "secret"
    app.register_blueprint(blueprint, url_prefix="/login")

    @app.route("/")
    def index():
        return "index"

    return app, blueprint


def test_generate_login_url():
    app, _ = make_app()
    with app.test_request_context("/"):
        login_url = flask.url_for("test-service.login")
        assert login_url == "/login/test-service"


def test_override_login_url():
    app, _ = make_app(login_url="/crazy/custom/url")
    with app.test_request_context("/"):
        login_url = flask.url_for("test-service.login")
        assert login_url == "/login/crazy/custom/url"


@responses.activate
def test_login_url():
    responses.add(
        responses.POST,
        "https://example.com/oauth/request_token",
        body="oauth_token=foobar&oauth_token_secret=bazqux",
    )
    app, _ = make_app()
    client = app.test_client()
    resp = client.get(
        "/login/test-service", base_url="https://a.b.c", follow_redirects=False
    )
    # check that we obtained a request token
    assert len(responses.calls) == 1
    assert "Authorization" in responses.calls[0].request.headers
    auth_header = dict(
        parse_authorization_header(
            responses.calls[0].request.headers["Authorization"].decode("utf-8")
        )
    )
    assert auth_header["oauth_consumer_key"] == "client_key"
    assert "oauth_signature" in auth_header
    assert auth_header["oauth_callback"] == quote_plus(
        "https://a.b.c/login/test-service/authorized"
    )
    # check that we redirected the client
    assert resp.status_code == 302
    assert (
        resp.headers["Location"]
        == "https://example.com/oauth/authorize?oauth_token=foobar"
    )


@responses.activate
def test_login_url_forwarded_proto():
    responses.add(
        responses.POST,
        "https://example.com/oauth/request_token",
        body="oauth_token=foobar&oauth_token_secret=bazqux",
    )
    app, _ = make_app()
    app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
    with app.test_client() as client:
        resp = client.get(
            "/login/test-service",
            base_url="http://a.b.c",
            headers={"X-Forwarded-Proto": "https"},
            follow_redirects=False,
        )
    auth_header = dict(
        parse_authorization_header(
            responses.calls[0].request.headers["Authorization"].decode("utf-8")
        )
    )
    # this should be https
    assert auth_header["oauth_callback"] == quote_plus(
        "https://a.b.c/login/test-service/authorized"
    )


@responses.activate
def test_authorized_url():
    responses.add(
        responses.POST,
        "https://example.com/oauth/access_token",
        body="oauth_token=xxx&oauth_token_secret=yyy",
    )
    app, _ = make_app()
    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz",
            base_url="https://a.b.c",
        )
        # check that we redirected the client
        assert resp.status_code == 302
        assert resp.headers["Location"] in ("https://a.b.c/", "/")
        # check that we obtained an access token
        assert len(responses.calls) == 1
        assert "Authorization" in responses.calls[0].request.headers
        auth_header = dict(
            parse_authorization_header(
                responses.calls[0].request.headers["Authorization"].decode("utf-8")
            )
        )
        assert auth_header["oauth_consumer_key"] == "client_key"
        assert auth_header["oauth_token"] == "foobar"
        assert auth_header["oauth_verifier"] == "xyz"
        # check that we stored the access token and secret in the session
        with client.session_transaction() as sess:
            assert sess["test-service_oauth_token"] == {
                "oauth_token": "xxx",
                "oauth_token_secret": "yyy",
            }


@responses.activate
def test_redirect_url():
    responses.add(
        responses.POST,
        "https://example.com/oauth/access_token",
        body="oauth_token=xxx&oauth_token_secret=yyy",
    )
    blueprint = OAuth1ConsumerBlueprint(
        "test-service",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_url="http://mysite.cool/whoa?query=basketball",
    )
    app = flask.Flask(__name__)
    app.secret_key = "secret"
    app.register_blueprint(blueprint, url_prefix="/login")

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz",
            base_url="https://a.b.c",
        )
        # check that we redirected the client
        assert resp.status_code == 302
        assert resp.headers["Location"] == "http://mysite.cool/whoa?query=basketball"


@responses.activate
def test_redirect_to():
    responses.add(
        responses.POST,
        "https://example.com/oauth/access_token",
        body="oauth_token=xxx&oauth_token_secret=yyy",
    )
    blueprint = OAuth1ConsumerBlueprint(
        "test-service",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_to="my_view",
    )
    app = flask.Flask(__name__)
    app.secret_key = "secret"
    app.register_blueprint(blueprint, url_prefix="/login")

    @app.route("/blargl")
    def my_view():
        return "check out my url"

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz",
            base_url="https://a.b.c",
        )
        # check that we redirected the client
        assert resp.status_code == 302
        assert resp.headers["Location"] in ("https://a.b.c/blargl", "/blargl")


@responses.activate
def test_redirect_fallback():
    responses.add(
        responses.POST,
        "https://example.com/oauth/access_token",
        body="oauth_token=xxx&oauth_token_secret=yyy",
    )
    blueprint = OAuth1ConsumerBlueprint(
        "test-service",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
    )
    app = flask.Flask(__name__)
    app.secret_key = "secret"
    app.register_blueprint(blueprint, url_prefix="/login")

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz",
            base_url="https://a.b.c",
        )
        # check that we redirected the client
        assert resp.status_code == 302
        assert resp.headers["Location"] in ("https://a.b.c/", "/")


def test_authorization_required_decorator_allowed():
    app, blueprint = make_app()

    @app.route("/restricted")
    @blueprint.session.authorization_required
    def restricted_view():
        return "allowed"

    blueprint.storage = MemoryStorage(
        {"oauth_token": "test1", "oauth_token_secret": "test2"}
    )

    with app.test_client() as client:
        resp = client.get("/restricted", base_url="https://a.b.c")
        assert resp.status_code == 200
        text = resp.get_data(as_text=True)
        assert text == "allowed"


def test_authorization_required_decorator_redirect():
    app, blueprint = make_app()

    @app.route("/restricted")
    @blueprint.session.authorization_required
    def restricted_view():
        return "allowed"

    with app.test_client() as client:
        resp = client.get("/restricted", base_url="https://a.b.c")
        # check that we redirected the client
        assert resp.status_code == 302
        assert resp.headers["Location"] in (
            "https://a.b.c/login/test-service",
            "/login/test-service",
        )


@requires_blinker
def test_signal_oauth_authorized(request):
    app, bp = make_app()
    fake_token = {"access_token": "test-token"}
    bp.session.fetch_access_token = mock.Mock(return_value=fake_token)

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))

    oauth_authorized.connect(callback)
    request.addfinalizer(lambda: oauth_authorized.disconnect(callback))

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )
        # check that we stored the token
        assert flask.session["test-service_oauth_token"] == fake_token

    assert len(calls), 1
    assert calls[0][0] == (bp,)
    assert calls[0][1] == {"token": fake_token}


@requires_blinker
def test_signal_oauth_authorized_abort(request):
    app, bp = make_app()
    bp.session.fetch_access_token = mock.Mock(return_value="test-token")

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))
        return False

    oauth_authorized.connect(callback)
    request.addfinalizer(lambda: oauth_authorized.disconnect(callback))

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )
        # check that we did NOT store the token
        assert "test-token_oauth_token" not in flask.session

    # the callback should still have been called
    assert len(calls) == 1


@requires_blinker
def test_signal_oauth_before_login(request):
    app, bp = make_app()
    bp.session.fetch_request_token = mock.Mock(return_value="test-token")

    def callback(*args, **kwargs):
        del flask.session["user_id"]
        return False

    oauth_before_login.connect(callback)
    request.addfinalizer(lambda: oauth_before_login.disconnect(callback))
    with app.test_request_context():
        with app.test_client() as client:
            flask.session["user_id"] = 1
            assert flask.session["user_id"] == 1
            client.get("/login/test-service")
            assert "user_id" not in flask.session


@requires_blinker
def test_signal_oauth_authorized_response(request):
    app, bp = make_app()
    bp.session.fetch_access_token = mock.Mock(return_value="test-token")

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))
        return flask.redirect("/url")

    oauth_authorized.connect(callback)
    request.addfinalizer(lambda: oauth_authorized.disconnect(callback))

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )
        assert resp.status_code == 302
        assert resp.headers["Location"] in ("/url", "http://localhost/url")
        # check that we did NOT store the token
        assert "test-token_oauth_token" not in flask.session

    # the callback should still have been called
    assert len(calls) == 1


@requires_blinker
def test_signal_sender_oauth_authorized(request):
    app, bp = make_app()
    bp2 = OAuth1ConsumerBlueprint(
        "test2",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_to="index",
    )
    app.register_blueprint(bp2, url_prefix="/login")

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))

    oauth_authorized.connect(callback, sender=bp)
    request.addfinalizer(lambda: oauth_authorized.disconnect(callback, sender=bp))

    with app.test_client() as client:
        bp.session.fetch_access_token = mock.Mock(return_value="test-token")
        bp2.session.fetch_access_token = mock.Mock(return_value="test2-token")
        resp = client.get(
            "/login/test2/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )

    assert len(calls) == 0

    with app.test_client() as client:
        bp.session.fetch_access_token = mock.Mock(return_value="test-token")
        bp2.session.fetch_access_token = mock.Mock(return_value="test2-token")
        resp = client.get(
            "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )

    assert len(calls) == 1
    assert calls[0][0] == (bp,)
    assert calls[0][1] == {"token": "test-token"}

    with app.test_client() as client:
        bp.session.fetch_access_token = mock.Mock(return_value="test-token")
        bp2.session.fetch_access_token = mock.Mock(return_value="test2-token")
        resp = client.get(
            "/login/test2/authorized?oauth_token=foobar&oauth_verifier=xyz"
        )

    assert len(calls) == 1  # unchanged


@requires_blinker
@responses.activate
def test_signal_oauth_error_login(request):
    responses.add(
        responses.POST,
        "https://example.com/oauth/request_token",
        body="oauth_problem=nonce_used",
        status=401,
    )
    app, bp = make_app()

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))

    oauth_error.connect(callback)
    request.addfinalizer(lambda: oauth_error.disconnect(callback))

    with app.test_client() as client:
        resp = client.get("/login/test-service", base_url="https://a.b.c")

    assert len(calls) == 1
    assert calls[0][0] == (bp,)
    assert (
        calls[0][1]["message"]
        == "Token request failed with code 401, response was 'oauth_problem=nonce_used'."
    )
    assert resp.status_code == 302
    location = resp.headers["Location"]
    assert location in ("/", "https://a.b.c/")


@requires_blinker
@responses.activate
def test_signal_oauth_error_authorized(request):
    responses.add(
        responses.POST,
        "https://example.com/oauth/access_token",
        body="Invalid request token.",
        status=401,
    )
    app, bp = make_app()

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))

    oauth_error.connect(callback)
    request.addfinalizer(lambda: oauth_error.disconnect(callback))

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?"
            "oauth_token=faketoken&"
            "oauth_token_secret=fakesecret&"
            "oauth_verifier=fakeverifier",
            base_url="https://a.b.c",
        )

    assert len(calls) == 1
    assert calls[0][0] == (bp,)
    assert (
        calls[0][1]["message"]
        == "Token request failed with code 401, response was 'Invalid request token.'."
    )
    assert resp.status_code == 302


@requires_blinker
def test_signal_oauth_notoken_authorized(request):
    app, bp = make_app()

    calls = []

    def callback(*args, **kwargs):
        calls.append((args, kwargs))

    oauth_error.connect(callback)
    request.addfinalizer(lambda: oauth_error.disconnect(callback))

    with app.test_client() as client:
        resp = client.get(
            "/login/test-service/authorized?" "denied=faketoken",
            base_url="https://a.b.c",
        )

    assert len(calls) == 1
    assert calls[0][0] == (bp,)
    assert "Response does not contain a token" in calls[0][1]["message"]
    assert calls[0][1]["response"] == {"denied": "faketoken"}
    assert resp.status_code == 302
    location = resp.headers["Location"]
    assert location in ("/", "https://a.b.c/")


class CustomOAuth1Session(OAuth1Session):
    my_attr = "foobar"


def test_custom_session_class():
    bp = OAuth1ConsumerBlueprint(
        "test",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_to="index",
        session_class=CustomOAuth1Session,
    )
    assert isinstance(bp.session, CustomOAuth1Session)
    assert bp.session.my_attr == "foobar"


def test_rule_kwargs():
    blueprint = OAuth1ConsumerBlueprint(
        "test-service",
        __name__,
        client_key="client_key",
        client_secret="client_secret",
        base_url="https://example.com",
        request_token_url="https://example.com/oauth/request_token",
        access_token_url="https://example.com/oauth/access_token",
        authorization_url="https://example.com/oauth/authorize",
        redirect_to="my_view",
        rule_kwargs={"host": "example2.com"},
    )
    app = flask.Flask(__name__)
    app.secret_key = "secret"
    app.register_blueprint(blueprint, url_prefix="/login")
    rules = [
        rule
        for rule in app.url_map.iter_rules()
        if rule.endpoint.startswith("test-service.")
    ]
    assert all(rule.host == "example2.com" for rule in rules)
    assert len(rules) == 2
