# pyright: reportAttributeAccessIssue=false

import base64
import datetime
import json
import os
import sys
from calendar import timegm
from urllib.parse import urlparse

import jwt
from httpretty import HTTPretty

from social_core.backends.open_id_connect import OpenIdConnectAuth

from ...exceptions import AuthTokenError
from ...utils import parse_qs
from .oauth import BaseAuthUrlTestMixin, OAuth2Test

sys.path.insert(0, "..")


TEST_ROOT = os.path.dirname(os.path.dirname(__file__))

JWK_KEY = {
    "kty": "RSA",
    "d": "ZmswNokEvBcxW_Kvcy8mWUQOQCBdGbnM0xR7nhvGHC-Q24z3XAQWlMWbsmGc_R1o"
    "_F3zK7DBlc3BokdRaO1KJirNmnHCw5TlnBlJrXiWpFBtVglUg98-4sRRO0VWnGXK"
    "JPOkBQ6b_DYRO3b0o8CSpWowpiV6HB71cjXTqKPZf-aXU9WjCCAtxVjfIxgQFu5I"
    "-G1Qah8mZeY8HK_y99L4f0siZcbUoaIcfeWBhxi14ODyuSAHt0sNEkhiIVBZE7QZ"
    "m-SEP1ryT9VAaljbwHHPmg7NC26vtLZhvaBGbTTJnEH0ZubbN2PMzsfeNyoCIHy4"
    "4QDSpQDCHfgcGOlHY_t5gQ",
    "e": "AQAB",
    "use": "sig",
    "kid": "testkey",
    "alg": "RS256",
    "n": "pUfcJ8WFrVue98Ygzb6KEQXHBzi8HavCu8VENB2As943--bHPcQ-nScXnrRFAUg8"
    "H5ZltuOcHWvsGw_AQifSLmOCSWJAPkdNb0w0QzY7Re8NrPjCsP58Tytp5LicF0Ao"
    "Ag28UK3JioY9hXHGvdZsWR1Rp3I-Z3nRBP6HyO18pEgcZ91c9aAzsqu80An9X4DA"
    "b1lExtZorvcd5yTBzZgr-MUeytVRni2lDNEpa6OFuopHXmg27Hn3oWAaQlbymd4g"
    "ifc01oahcwl3ze2tMK6gJxa_TdCf1y99Yq6oilmVvZJ8kwWWnbPE-oDmOVPVnEyT"
    "vYVCvN4rBT1DQ-x0F1mo2Q",
}

JWK_PUBLIC_KEY = {key: value for key, value in JWK_KEY.items() if key != "d"}


class OpenIdConnectTestMixin:
    """
    Mixin to test OpenID Connect consumers. Inheriting classes should also
    inherit OAuth2Test.
    """

    client_key = "a-key"
    client_secret = "a-secret-key"
    issuer = None  # id_token issuer
    openid_config_body = None
    key = None

    # Avoid sharing access_token_kwargs between different subclasses
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.access_token_kwargs = getattr(cls, "access_token_kwargs", {})

    def setUp(self):
        super().setUp()
        self.key = JWK_KEY.copy()
        self.public_key = JWK_PUBLIC_KEY.copy()

        assert self.openid_config_body, "openid_config_body must be set"

        HTTPretty.register_uri(
            HTTPretty.GET,
            self.backend.oidc_endpoint() + "/.well-known/openid-configuration",
            status=200,
            body=self.openid_config_body,
        )
        oidc_config = json.loads(self.openid_config_body)

        def jwks(_request, _uri, headers):
            return 200, headers, json.dumps({"keys": [self.key]})

        HTTPretty.register_uri(
            HTTPretty.GET,
            oidc_config.get("jwks_uri"),
            status=200,
            body=json.dumps({"keys": [self.public_key]}),
        )

    def extra_settings(self):
        settings = super().extra_settings()
        settings.update(
            {
                f"SOCIAL_AUTH_{self.name}_KEY": self.client_key,
                f"SOCIAL_AUTH_{self.name}_SECRET": self.client_secret,
                f"SOCIAL_AUTH_{self.name}_ID_TOKEN_DECRYPTION_KEY": self.client_secret,
            }
        )
        return settings

    def get_id_token(
        self,
        client_key=None,
        expiration_datetime=None,
        issue_datetime=None,
        nonce=None,
        issuer=None,
    ):
        """
        Return the id_token to be added to the access token body.
        """
        return {
            "iss": issuer,
            "nonce": nonce,
            "aud": client_key,
            "azp": client_key,
            "exp": expiration_datetime,
            "iat": issue_datetime,
            "sub": "1234",
        }

    def prepare_access_token_body(
        self,
        client_key=None,
        tamper_message=False,
        expiration_datetime=None,
        kid=None,
        issue_datetime=None,
        nonce=None,
        issuer=None,
    ):
        """
        Prepares a provider access token response. Arguments:

        client_id       -- (str) OAuth ID for the client that requested
                                 authentication.
        expiration_time -- (datetime) Date and time after which the response
                                      should be considered invalid.
        """

        body = {"access_token": "foobar", "token_type": "bearer"}
        client_key = client_key or self.client_key
        now = datetime.datetime.now(datetime.timezone.utc)
        expiration_datetime = expiration_datetime or (
            now + datetime.timedelta(seconds=30)
        )
        issue_datetime = issue_datetime or now
        nonce = nonce or "a-nonce"
        issuer = issuer or self.issuer
        id_token = self.get_id_token(
            client_key,
            timegm(expiration_datetime.timetuple()),
            timegm(issue_datetime.timetuple()),
            nonce,
            issuer,
        )
        # calc at_hash
        id_token["at_hash"] = OpenIdConnectAuth.calc_at_hash("foobar", "RS256")

        body["id_token"] = jwt.encode(
            id_token,
            key=jwt.PyJWK(
                dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce)  # type: ignore reportCallIssue
            ).key,
            algorithm="RS256",
            headers={"kid": kid} if kid else None,
        )

        if tamper_message:
            header, msg, sig = body["id_token"].split(".")
            id_token["sub"] = "1235"
            msg = base64.encodebytes(json.dumps(id_token).encode()).decode()
            body["id_token"] = f"{header}.{msg}.{sig}"

        return json.dumps(body)

    def authtoken_raised(self, expected_message, **access_token_kwargs):
        self.access_token_kwargs = access_token_kwargs
        with self.assertRaisesRegex(AuthTokenError, expected_message):
            self.do_login()

    def pre_complete_callback(self, start_url):
        nonce = parse_qs(urlparse(start_url).query)["nonce"]

        self.access_token_kwargs.setdefault("nonce", nonce)
        self.access_token_body = self.prepare_access_token_body(
            **self.access_token_kwargs
        )
        super().pre_complete_callback(start_url)

    def test_invalid_signature(self):
        self.authtoken_raised(
            "Token error: Signature verification failed", tamper_message=True
        )

    def test_expired_signature(self):
        expiration_datetime = datetime.datetime.now(
            datetime.timezone.utc
        ) - datetime.timedelta(seconds=30)
        self.authtoken_raised(
            "Token error: Signature has expired",
            expiration_datetime=expiration_datetime,
        )

    def test_invalid_issuer(self):
        self.authtoken_raised("Token error: Invalid issuer", issuer="someone-else")

    def test_invalid_audience(self):
        self.authtoken_raised(
            "Token error: Invalid audience", client_key="someone-else"
        )

    def test_invalid_issue_time(self):
        expiration_datetime = datetime.datetime.now(
            datetime.timezone.utc
        ) - datetime.timedelta(seconds=self.backend.ID_TOKEN_MAX_AGE * 2)
        self.authtoken_raised(
            "Token error: Incorrect id_token: iat", issue_datetime=expiration_datetime
        )

    def test_invalid_nonce(self):
        self.authtoken_raised(
            "Token error: Incorrect id_token: nonce",
            nonce="something-wrong",
            kid="testkey",
        )

    def test_invalid_kid(self):
        self.authtoken_raised(
            "Token error: Signature verification failed", kid="doesnotexist"
        )


class BaseOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin):
    backend_path = "social_core.backends.open_id_connect.OpenIdConnectAuth"
    issuer = "https://example.com"
    openid_config_body = json.dumps(
        {
            "issuer": "https://example.com",
            "authorization_endpoint": "https://example.com/oidc/auth",
            "token_endpoint": "https://example.com/oidc/token",
            "userinfo_endpoint": "https://example.com/oidc/userinfo",
            "revocation_endpoint": "https://example.com/oidc/revoke",
            "jwks_uri": "https://example.com/oidc/certs",
        }
    )

    expected_username = "cartman"

    def extra_settings(self):
        settings = super().extra_settings()
        settings.update(
            {
                "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": "https://example.com/oidc",
            }
        )
        return settings

    def pre_complete_callback(self, start_url):
        super().pre_complete_callback(start_url)
        HTTPretty.register_uri(
            "GET",
            uri=self.backend.userinfo_url(),
            status=200,
            body=json.dumps({"preferred_username": self.expected_username}),
            content_type="text/json",
        )

    def test_everything_works(self):
        self.do_login()


class ExampleOpenIdConnectAuth(OpenIdConnectAuth):
    name = "example123"
    OIDC_ENDPOINT = "https://example.com/oidc"


class ExampleOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test):
    backend_path = (
        "social_core.tests.backends.test_open_id_connect.ExampleOpenIdConnectAuth"
    )
    issuer = "https://example.com"
    openid_config_body = json.dumps(
        {
            "issuer": "https://example.com",
            "authorization_endpoint": "https://example.com/oidc/auth",
            "token_endpoint": "https://example.com/oidc/token",
            "userinfo_endpoint": "https://example.com/oidc/userinfo",
            "revocation_endpoint": "https://example.com/oidc/revoke",
            "jwks_uri": "https://example.com/oidc/certs",
        }
    )

    expected_username = "cartman"

    def pre_complete_callback(self, start_url):
        super().pre_complete_callback(start_url)
        HTTPretty.register_uri(
            "GET",
            uri=self.backend.userinfo_url(),
            status=200,
            body=json.dumps({"preferred_username": self.expected_username}),
            content_type="text/json",
        )

    def test_everything_works(self):
        self.do_login()
