import json
import logging
from unittest.mock import Mock

from pytest import fixture, mark, raises
from tornado import web
from traitlets.config import Config

from ..auth0 import Auth0OAuthenticator
from ..oauth2 import OAuthLogoutHandler
from .mocks import mock_handler, setup_oauth_mock

AUTH0_DOMAIN = "jupyterhub-test.auth0.com"


@fixture
def auth0_client(client):
    setup_oauth_mock(
        client,
        host=AUTH0_DOMAIN,
        access_token_path='/oauth/token',
        user_path='/userinfo',
    )
    return client


def user_model(extra=None):
    """Return a user model"""
    model = {
        "email": "user1@example.com",
        "name": "user1",
        "groups": ["group1"],
        "email_verified": True,
    }
    if extra:
        model.update(extra)
    return model


@mark.parametrize(
    "test_variation_id,class_config,expect_allowed,expect_admin",
    [
        # no allow config tested
        ("00", {}, False, None),
        # allow config, individually tested
        ("01", {"allow_all": True}, True, None),
        ("02", {"allowed_users": {"user1"}}, True, None),
        ("03", {"allowed_users": {"not-test-user"}}, False, None),
        ("04", {"admin_users": {"user1"}}, True, True),
        ("05", {"admin_users": {"not-test-user"}}, False, None),
        # allow config, some combinations of two tested
        (
            "10",
            {
                "allow_all": False,
                "allowed_users": {"not-test-user"},
            },
            False,
            None,
        ),
        (
            "11",
            {
                "admin_users": {"user1"},
                "allowed_users": {"not-test-user"},
            },
            True,
            True,
        ),
        # common tests with allowed_groups and manage_groups
        (
            "20",
            {
                "allowed_groups": {"group1"},
                "auth_state_groups_key": "auth0_user.groups",
                "manage_groups": True,
            },
            True,
            None,
        ),
        (
            "21",
            {
                "allowed_groups": {"test-user-not-in-group"},
                "auth_state_groups_key": "auth0_user.groups",
                "manage_groups": True,
            },
            False,
            None,
        ),
        (
            "22",
            {
                "admin_groups": {"group1"},
                "auth_state_groups_key": "auth0_user.groups",
                "manage_groups": True,
            },
            True,
            True,
        ),
        (
            "23",
            {
                "admin_groups": {"test-user-not-in-group"},
                "auth_state_groups_key": "auth0_user.groups",
                "manage_groups": True,
            },
            False,
            False,
        ),
    ],
)
async def test_auth0(
    auth0_client,
    test_variation_id,
    class_config,
    expect_allowed,
    expect_admin,
):
    c = Config()
    c.Auth0OAuthenticator = Config(class_config)
    c.Auth0OAuthenticator.auth0_domain = AUTH0_DOMAIN
    c.Auth0OAuthenticator.username_claim = "name"
    authenticator = Auth0OAuthenticator(config=c)

    handled_user_model = user_model()
    handler = auth0_client.handler_for_user(handled_user_model)
    auth_model = await authenticator.get_authenticated_user(handler, None)

    if expect_allowed:
        assert auth_model
        if authenticator.manage_groups:
            assert set(auth_model) == {"name", "admin", "auth_state", "groups"}
        else:
            assert set(auth_model) == {"name", "admin", "auth_state"}
        assert auth_model["admin"] == expect_admin
        auth_state = auth_model["auth_state"]
        assert json.dumps(auth_state)
        assert "access_token" in auth_state
        user_info = auth_state[authenticator.user_auth_state_key]
        assert user_info == handled_user_model
        assert auth_model["name"] == user_info[authenticator.username_claim]
    else:
        assert auth_model is None


@mark.parametrize(
    "email_verified, allow_unverified, expect_allowed",
    [
        (True, False, True),
        (False, False, False),
        (False, True, True),
    ],
)
async def test_email_verified(
    auth0_client, email_verified, allow_unverified, expect_allowed
):
    c = Config()
    c.Auth0OAuthenticator = Config()
    c.Auth0OAuthenticator.auth0_domain = AUTH0_DOMAIN
    c.Auth0OAuthenticator.username_claim = "email"
    c.Auth0OAuthenticator.allow_all = True
    if allow_unverified:
        c.Auth0OAuthenticator.allow_unverified_email = allow_unverified
    authenticator = Auth0OAuthenticator(config=c)
    assert authenticator.allow_unverified_email == allow_unverified

    handled_user_model = user_model({"email_verified": email_verified})
    handler = auth0_client.handler_for_user(handled_user_model)

    if expect_allowed:
        auth_model = await authenticator.get_authenticated_user(handler, None)
        assert auth_model is not None
        allowed = await authenticator.check_allowed(auth_model["name"], auth_model)
        assert allowed
    else:
        with raises(web.HTTPError) as exc_info:
            auth_model = await authenticator.get_authenticated_user(handler, None)
            assert auth_model is not None
            await authenticator.check_allowed(auth_model["name"], auth_model)
        assert exc_info.value.status_code == 403


async def test_custom_logout(monkeypatch):
    authenticator = Auth0OAuthenticator()
    logout_handler = mock_handler(OAuthLogoutHandler, authenticator=authenticator)
    monkeypatch.setattr(web.RequestHandler, 'redirect', Mock())

    logout_handler.clear_login_cookie = Mock()
    logout_handler.clear_cookie = Mock()
    logout_handler._jupyterhub_user = Mock()
    monkeypatch.setitem(logout_handler.settings, 'statsd', Mock())

    # Sanity check: Ensure the logout handler and url are set on the hub
    handlers = [handler for _, handler in authenticator.get_handlers(None)]
    assert any([h == OAuthLogoutHandler for h in handlers])
    assert authenticator.logout_url('http://myhost') == 'http://myhost/logout'

    # Check redirection to the custom logout url
    authenticator.auth0_domain = AUTH0_DOMAIN
    await logout_handler.get()
    custom_logout_url = f'https://{AUTH0_DOMAIN}/v2/logout'
    logout_handler.redirect.assert_called_with(custom_logout_url)


@mark.parametrize(
    "test_variation_id,class_config,expect_config,expect_loglevel,expect_message",
    [
        (
            "username_key",
            {"username_key": "dummy"},
            {"username_claim": "dummy"},
            logging.WARNING,
            "Auth0OAuthenticator.username_key is deprecated in Auth0OAuthenticator 16.0.0, use Auth0OAuthenticator.username_claim instead",
        ),
    ],
)
async def test_deprecated_config(
    caplog,
    test_variation_id,
    class_config,
    expect_config,
    expect_loglevel,
    expect_message,
):
    """
    Tests that a warning is emitted when using a deprecated config and that
    configuring the old config ends up configuring the new config.
    """
    print(f"Running test variation id {test_variation_id}")
    c = Config()
    c.Auth0OAuthenticator = Config(class_config)

    test_logger = logging.getLogger('testlog')
    if expect_loglevel == logging.ERROR:
        with raises(ValueError, match=expect_message):
            Auth0OAuthenticator(config=c, log=test_logger)
    else:
        authenticator = Auth0OAuthenticator(config=c, log=test_logger)
        for key, value in expect_config.items():
            assert getattr(authenticator, key) == value

    captured_log_tuples = caplog.record_tuples
    print(captured_log_tuples)

    expected_log_tuple = (test_logger.name, expect_loglevel, expect_message)
    assert expected_log_tuple in captured_log_tuples
