import urllib
import contextlib
from typing import Dict, List, Union, ContextManager

import flask
import flask_login
import pytest
from tests import AuthActions

from server.api.blueprints.login import (
    handle_oauth,
    validate_inputs,
    create_or_get_oauth,
)
from server.api.database.models import BlacklistToken, User, OAuth, Provider
from server.error_handling import RouteError, TokenError


@pytest.fixture
def fake_profile():
    return {
        "name": "test",
        "email": "t@t.com",
        "picture": {"data": {"url": "test.jpg"}},
        "provider_user_id": 1111,
    }


@pytest.fixture
def mock_oauth_user_id(responses, fake_profile, social_network):
    user_id = {"data": {"user_id": fake_profile["provider_user_id"]}}
    responses.add(
        responses.GET,
        social_network.base_url + social_network.token_metadata_url,
        status=200,
        json=user_id,
    )


@pytest.fixture
def mock_oauth_profile(responses, fake_profile, social_network):
    responses.add(
        responses.GET,
        f"{social_network.base_url}{fake_profile['provider_user_id']}",
        status=200,
        json=fake_profile,
    )


def test_normal_register(app, auth: AuthActions):
    resp = auth.register()
    assert "auth_token" in resp.json
    assert "refresh_token" in resp.json
    assert "user" in resp.json
    with app.app_context():
        assert User.query.filter_by(email="test@test.com").one()


def test_login(user, auth):
    resp = auth.login()
    assert "auth_token" in resp.json
    assert "refresh_token" in resp.json
    assert user.to_dict()["id"] == resp.json["user"]["id"]
    payload = User.decode_token(resp.json["auth_token"])
    assert "email" in payload


def test_login_validate_input(auth: AuthActions):
    resp = auth.login("t@aaa.com", "123")
    assert resp.json.get("message") == "Invalid email or password."


def test_encode_auth_token(user):
    auth_token = user.encode_auth_token()
    assert isinstance(auth_token, bytes)


def test_decode_auth_token(user):
    auth_token = user.encode_auth_token()
    assert isinstance(auth_token, bytes)
    assert User.from_login_token(auth_token.decode("utf-8")) == user


def test_logout(auth: AuthActions):
    login = auth.login()
    resp = auth.logout()
    assert "Logged out successfully" in resp.json.get("message")
    # check that token was blacklisted
    refresh_token = login.json.get("refresh_token")
    auth_token = login.json.get("auth_token")
    assert len(BlacklistToken.query.all()) == 2
    assert all(
        (
            token.token == auth_token or token.token == refresh_token
            for token in BlacklistToken.query.all()
        )
    )


def test_blacklist_token(app, auth: AuthActions):
    resp_login = auth.login()
    with app.app_context():
        blacklist_token = BlacklistToken(token=resp_login.json["auth_token"])
        blacklist_token.save()
        resp = auth.logout()
        assert "BLACKLISTED_TOKEN" in str(resp.json)


def test_invalid_token(auth: AuthActions):
    resp = auth.logout(headers={"Authorization": "Bearer NOPE"})
    assert "INVALID_TOKEN" in resp.json.get("message")


@pytest.mark.parametrize(
    ("email", "password", "name", "area", "message"),
    (
        ("", "bb", "test", "test", "Email is required."),
        ("tt@t.com", "", "test", "test", "Password is required."),
        ("ttom", "a", "test", "test", "Email is not valid."),
        ("test", "a", "test", "", "Area is required."),
        ("test", "a", "", "test", "Name is required."),
        ("t@test.com", "a", "a", "A", "Email is already registered."),
    ),
)
def test_register_validate_input(
    auth: AuthActions, email, password, name, area, message
):
    resp = auth.register(email=email, password=password, name=name, area=area)
    assert message in resp.json.get("message")


def test_exchange_token(requester, user: User):
    resp = requester.post(
        "/login/exchange_token",
        json={"exchange_token": user.encode_exchange_token().decode()},
    )
    assert "user" in resp.json
    assert "auth_token" in resp.json
    assert user == User.from_login_token(resp.json["auth_token"])


def test_refresh_token_payload(auth, user: User):
    """check that refresh token after login
    has a scope and a user_id"""
    resp = auth.login()
    assert resp.json["refresh_token"]
    payload = User.decode_token(resp.json["refresh_token"])
    assert payload["scope"]
    assert User.from_payload(payload) == user


def test_refresh_token_endpoint(auth, requester):
    """check that a valid refresh token
    generates a new auth token, and an invalid token
    doesn't"""
    auth.login()
    resp = requester.post(
        "/login/refresh_token", json={"refresh_token": auth.refresh_token}
    )
    assert resp.json["auth_token"]
    resp = requester.post("/login/refresh_token", json={"refresh_token": "none"})
    assert "INVALID_TOKEN" in resp.json["message"]


def test_refresh_without_refresh_token(requester):
    """call refresh endpoint without token"""
    resp = requester.post("/login/refresh_token", json={"refresh_token": ""})
    assert "INAVLID_REFRESH_TOKEN" == resp.json["message"]


def test_validate_inputs():
    with pytest.raises(RouteError):
        validate_inputs({"name": "test", "area": "test", "email": "test"})
        validate_inputs({"name": "test"})
        validate_inputs({"name": "test"}, required=["phone"])
    assert validate_inputs({"name": "test"}, required=["name"])
    assert validate_inputs({"name": "test", "area": "test"}, required=["name", "area"])
    assert validate_inputs({"name": "test", "area": "test"}, required=[])


def test_edit_data(app, user, requester, auth: AuthActions):
    auth.login()
    resp = requester.post("/login/edit_data", json={"name": "new", "phone": "0533333"})
    assert "new" == resp.json["data"]["name"]
    assert user.area == resp.json["data"]["area"]
    assert user.phone == "0533333"
    resp = requester.post("/login/edit_data", json={"area": "new"})
    assert "new" == resp.json["data"]["area"]
    assert requester.post("/login/edit_data", json={})
    assert requester.post("/login/edit_data", json={"password": "new"})


def test_oauth_first_step(client, auth, requester, social_network):
    with client:
        auth.login()
        resp = requester.get(f"/login/{social_network.network_name}")
        assert resp.status_code == 302  # redirect
        assert flask.session["state"]
        auth.logout()
        assert flask_login.current_user.is_authenticated
    assert not flask_login.current_user.is_authenticated


def test_register_with_oauth(
    app: flask.Flask,
    fake_profile,
    mock_oauth_user_id,
    mock_oauth_profile,
    fake_token,
    social_network,
):
    """Tests that a new user was registered"""
    with app.test_request_context("/"):
        resp = handle_oauth(social_network, fake_token)
        user = User.query.filter_by(name=fake_profile["name"]).first()
        assert user.email
        assert "token=" in resp.headers["Location"]


def test_login_with_oauth(
    app: flask.Flask,
    fake_token,
    user,
    mock_oauth_user_id,
    fake_profile,
    requester,
    social_network,
):
    length = len(User.query.all())
    OAuth.create(
        provider=getattr(Provider, social_network.network_name),
        provider_user_id=fake_profile["provider_user_id"],
        token=fake_token,
        user=user,
    )
    with app.test_request_context("/"):
        resp = handle_oauth(social_network, fake_token)
        assert len(User.query.all()) == length
        assert "token" in resp.headers["Location"]


def test_oauth_no_token(social_network):
    resp = handle_oauth(social_network, None)
    assert urllib.parse.quote("No token") in resp.headers["Location"]


@contextlib.contextmanager
def logged_in_context(app, auth, endpoint="", **kwargs) -> ContextManager[None]:
    """A manager to enter and exit a logged-in request context"""
    auth.login(**kwargs)
    with app.test_request_context(
        f"/{endpoint}", headers={"Authorization": f"Bearer {auth.auth_token}"}
    ) as fp:
        yield fp
        auth.logout()


def test_oauth_session(
    app: flask.Flask,
    auth,
    fake_token,
    mock_oauth_profile,
    mock_oauth_user_id,
    social_network,
):
    with logged_in_context(app, auth, f"login/{social_network.network_name}"):
        assert flask_login.current_user.is_authenticated
        handle_oauth(social_network, fake_token)
        assert not flask_login.current_user.is_authenticated


def test_create_or_get_oauth(user, app, fake_profile, fake_token, social_network):
    with app.app_context():
        oauth = create_or_get_oauth(
            social_network.network_name, fake_profile["provider_user_id"], fake_token
        )
        assert oauth.token == fake_token
        OAuth.create(
            provider=getattr(Provider, social_network.network_name),
            provider_user_id=fake_profile["provider_user_id"],
            token=fake_token,
            user=user,
        )
        oauth = create_or_get_oauth(
            social_network.network_name, fake_profile["provider_user_id"], fake_token
        )
        assert oauth.user == user
