import string

from tests import unittest

import msal
from msal import oauth2cli
from msal.oauth2cli.oauth2 import _generate_pkce_code_verifier


class TestCsprngUsage(unittest.TestCase):
    """Tests that security-critical parameters use cryptographically secure randomness."""

    # RFC 7636 §4.1: code_verifier = 43*128unreserved
    _PKCE_ALPHABET = set(string.ascii_letters + string.digits + "-._~")

    def test_pkce_code_verifier_contains_only_valid_characters(self):
        for _ in range(50):
            result = _generate_pkce_code_verifier()
            self.assertTrue(
                set(result["code_verifier"]).issubset(self._PKCE_ALPHABET),
                "code_verifier contains invalid characters")

    def test_pkce_code_verifier_has_correct_default_length(self):
        result = _generate_pkce_code_verifier()
        self.assertEqual(len(result["code_verifier"]), 43)

    def test_pkce_code_verifier_respects_custom_length(self):
        for length in (43, 64, 128):
            result = _generate_pkce_code_verifier(length)
            self.assertEqual(len(result["code_verifier"]), length)

    def test_pkce_code_verifier_can_have_repeated_characters(self):
        """secrets.choice() samples with replacement, unlike the old random.sample()."""
        result = _generate_pkce_code_verifier(128)
        code_verifier = result["code_verifier"]
        self.assertLess(len(set(code_verifier)), len(code_verifier),
            "At length 128 with a 66-char alphabet, repeated chars are expected")

    def test_pkce_code_verifier_is_not_deterministic(self):
        results = {_generate_pkce_code_verifier()["code_verifier"] for _ in range(10)}
        self.assertGreater(len(results), 1, "code_verifier should not be deterministic")

    def test_oauth2_state_is_url_safe_and_unpredictable(self):
        """State generated by initiate_auth_code_flow should be URL-safe."""
        from msal.oauth2cli.oauth2 import Client
        client = Client(
            {"authorization_endpoint": "https://example.com/auth",
             "token_endpoint": "https://example.com/token"},
            client_id="test_client")
        states = set()
        for _ in range(10):
            flow = client.initiate_auth_code_flow(
                redirect_uri="http://localhost", scope=["openid"],
                response_mode="form_post")
            state = flow["state"]
            self.assertRegex(state, r'^[A-Za-z0-9_-]+$',
                "state should be URL-safe")
            states.add(state)
        self.assertGreater(len(states), 1, "state should not be deterministic")

    def test_oidc_nonce_is_url_safe_and_unpredictable(self):
        """Nonce generated by OIDC initiate_auth_code_flow should be URL-safe."""
        from msal.oauth2cli.oidc import Client
        client = Client(
            {"authorization_endpoint": "https://example.com/auth",
             "token_endpoint": "https://example.com/token"},
            client_id="test_client")
        nonces = set()
        for _ in range(10):
            flow = client.initiate_auth_code_flow(
                redirect_uri="http://localhost", scope=["openid"],
                response_mode="form_post")
            nonce = flow["nonce"]
            self.assertRegex(nonce, r'^[A-Za-z0-9_-]+$',
                "nonce should be URL-safe")
            nonces.add(nonce)
        self.assertGreater(len(nonces), 1, "nonce should not be deterministic")


class TestIdToken(unittest.TestCase):
    EXPIRED_ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE3MDY1NzA3MzIsImV4cCI6MTY3NDk0ODMzMiwiYXVkIjoiZm9vIiwic3ViIjoic3ViamVjdCJ9.wyWNFxnE35SMP6FpxnWZmWQAy4KD0No_Q1rUy5bNnLs"

    def test_id_token_should_tolerate_time_error(self):
        self.assertEqual(oauth2cli.oidc.decode_id_token(self.EXPIRED_ID_TOKEN), {
            "iss": "issuer",
            "iat": 1706570732,
            "exp": 1674948332,  # 2023-1-28
            "aud": "foo",
            "sub": "subject",
            }, "id_token is decoded correctly, without raising exception")

    def test_id_token_should_error_out_on_client_id_error(self):
        with self.assertRaises(msal.IdTokenError):
            oauth2cli.oidc.decode_id_token(self.EXPIRED_ID_TOKEN, client_id="not foo")

