# type: ignore
import os
import unittest
from time import time
from unittest.mock import AsyncMock, MagicMock, patch

from flask.testing import FlaskClient
from jwt import encode
from pydantic import ValidationError
from werkzeug.test import TestResponse

from corbado_python_sdk.entities.user_entity import UserEntity
from corbado_python_sdk.services.implementation import SessionService

from ..utils.session_service_example import app

TEST_NAME = "Test Name"
TEST_EMAIL = "test@email.com"
TEST_PHONE_NUMBER = "+012345678"
TEST_USER_ID = "12345"


class TestBase(unittest.TestCase):
    """Base class for all test using mocking for SessionService.

    Args:
        unittest (_type_): unittest

    Raises:
        FileNotFoundError: If no private key was found

    """

    mock_urlopen = None

    @classmethod
    def setUpClass(cls) -> None:
        # Setup, read private key and jwks for mocking
        cls.session_service: SessionService = cls.create_session_service()

        jwks_path: str = os.path.join(os.path.dirname(__file__), "test_data", "jwks.json")
        try:
            with open(file=jwks_path, mode="rb") as private_key_file:
                cls.jwks: bytes = private_key_file.read()
        except FileNotFoundError:
            raise FileNotFoundError("Failed to read private key file")

    def setUp(self) -> None:
        self.my_patch = patch("urllib.request.urlopen")
        self.mock_urlopen: MagicMock | AsyncMock = self.my_patch.start()
        self.addCleanup(self.my_patch.stop)
        # Mock the response
        self.mock_response = MagicMock()
        self.mock_response.__enter__.return_value = self.mock_response
        self.mock_response.read.return_value = self.jwks
        self.mock_urlopen.return_value = self.mock_response

    @classmethod
    def create_session_service(cls) -> SessionService:
        """Create test configuration of SessionService.

        Warning! You should normally use SessionInterface from CorbadoSDK for non-test purposes.

        Returns:
            SessionService: SessionService instance
        """
        return SessionService(
            issuer="https://auth.acme.com",
            jwks_uri="https://example_uri.com",  # does not matter, url access is mocked
            short_session_cookie_name="cbo_short_session",
        )

    def tearDown(self) -> None:
        self.my_patch.stop()

    def _provide_jwts(self):
        """Provide list of jwts with expected test results."""
        return [
            # JWT with invalid format
            (False, "invalid"),
            # JWT signed with wrong algorithm (HS256 instead of RS256)
            (
                False,
                """eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6
                IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao""",
            ),
            # Not before (nfb) in future
            (False, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) + 100)),
            # Expired (exp)
            (False, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) - 100, nbf=int(time()) - 100)),
            # Invalid issuer (iss)
            (False, self._generate_jwt(iss="https://invalid.com", exp=int(time()) + 100, nbf=int(time()) - 100)),
            # Success
            (True, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100)),
        ]

    @classmethod
    def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str:
        payload = {
            "iss": iss,
            "iat": int(time()),
            "exp": exp,
            "nbf": nbf,
            "sub": TEST_USER_ID,
            "name": TEST_NAME,
            "email": TEST_EMAIL,
            "phone_number": TEST_PHONE_NUMBER,
        }

        private_key_path: str = os.path.join(os.path.dirname(__file__), "test_data", "privateKey.pem")
        try:
            with open(file=private_key_path, mode="rb") as private_key_file:
                private_key = private_key_file.read()
        except FileNotFoundError:
            raise FileNotFoundError("Failed to read private key file")

        return encode(payload, private_key, algorithm="RS256", headers={"kid": "kid123"})


class TestSessionService(TestBase):
    def test_get_and_validate_short_session_value(self):
        for valid, token in self._provide_jwts():
            result: UserEntity = self.session_service.get_and_validate_short_session_value(short_session=token)

            self.assertEqual(valid, result.authenticated)

            if valid:
                self.assertEqual(first=TEST_PHONE_NUMBER, second=result.phone_number)
                self.assertEqual(first=TEST_EMAIL, second=result.email)
                self.assertEqual(first=TEST_NAME, second=result.name)
                self.assertEqual(TEST_USER_ID, result.user_id)

    def test_cache_jwk_set_used_expect_reduced_urlopen_calls(self):
        jwt: str = self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100)
        self.session_service.get_and_validate_short_session_value(short_session=jwt)
        num_calls: int = self.mock_urlopen.call_count
        for _i in range(3):
            self.session_service.get_and_validate_short_session_value(short_session=jwt)
        self.assertEqual(num_calls, self.mock_urlopen.call_count)

    def test_generate_jwt(self):
        iss = "issuer"
        exp = 1234567890  # Example expiration time
        nbf = 0  # Example not-before time

        jwt_token: str = self._generate_jwt(iss=iss, exp=exp, nbf=nbf)
        self.assertIsNotNone(jwt_token)

    def test_init_parameters(self):
        test_cases = [
            # Valid session service
            ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "name"}, True),
            # Test empty issuer
            ({"issuer": "", "jwks_uri": "2", "short_session_cookie_name": "name"}, False),
            # Test empty jwks_uri
            ({"issuer": "s", "jwks_uri": "", "short_session_cookie_name": "name"}, False),
            # Test empty short_session_cookie_name
            ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": ""}, False),
        ]

        for params, expected_result in test_cases:
            if expected_result:
                # No exception should be raised
                assert isinstance(SessionService(**params), SessionService)
            else:
                # ValidationError should be raised
                with self.assertRaises(ValidationError):
                    SessionService(**params)


class TestApp(unittest.TestCase):
    def setUp(self):
        self.app: FlaskClient = app.test_client()

    def test_index(self):
        response: TestResponse = self.app.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertIn(b"Hello world!", response.data)

    def test_set_cookie(self):
        response = self.app.get("/setCookie")
        self.assertEqual(response.status_code, 200)
        self.assertIn(b"Cookie set!", response.data)
