#
# Copyright (c) 2021 the Hugging Face team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.#
import asyncio
import os
import unittest
from typing import Any, Dict
from unittest.mock import patch

from fastapi.exceptions import HTTPException
from fastapi.security.http import HTTPAuthorizationCredentials
from requests import ConnectionError
from requests.models import HTTPError
from starlette.responses import Response
from starlette.status import HTTP_401_UNAUTHORIZED

from app.services.authentication import authenticate


class AsyncTestCase(unittest.TestCase):
    """Utility to run async tests"""

    def __init__(self, methodName="runTest", loop=None):
        self.loop = loop or asyncio.get_event_loop()
        self._function_cache = {}
        super().__init__(methodName=methodName)

    def coroutine_function_decorator(self, func):
        def wrapper(*args, **kw):
            return self.loop.run_until_complete(func(*args, **kw))

        return wrapper

    def __getattribute__(self, item):
        attr = object.__getattribute__(self, item)
        if asyncio.iscoroutinefunction(attr):
            if item not in self._function_cache:
                self._function_cache[item] = self.coroutine_function_decorator(attr)
            return self._function_cache[item]
        return attr


class TestAuthenticate(AsyncTestCase):
    def setUp(self):
        super().setUp()
        self.moonlanding_mock = patch("app.services.authentication.moonlanding_auth").start()
        getenv_mock = patch("os.getenv").start()
        getenv_mock.side_effect = lambda name: os.getenv(name) if name != "PRODUCTION" else "1"
        self.addCleanup(patch.stopall)
        self.base_scope = {"type": "http", "path": "/", "headers": []}

    def set_moonlanding_response(
        self,
    ):
        # TODO non-user
        resp: Dict[str, Any] = {"type": "user", "name": "autotest", "email": "auto@test.co", "orgs": []}

        self.moonlanding_mock.return_value = resp

    async def test_validation(self):
        # Invalid credentials
        creds = None
        with self.assertRaises(HTTPException) as exc_info:
            await authenticate(creds)
        self.assertEqual(exc_info.exception.status_code, HTTP_401_UNAUTHORIZED)

        # Wrong prefix
        creds = HTTPAuthorizationCredentials(scheme="wrong", credentials="api_fake")
        with self.assertRaises(HTTPException) as exc_info:
            await authenticate(creds)
        self.assertEqual(exc_info.exception.status_code, HTTP_401_UNAUTHORIZED)

    async def test_moonlanding_error(self):
        creds = HTTPAuthorizationCredentials(scheme="Bearer", credentials="api_fake")
        # Connection error
        self.moonlanding_mock.side_effect = ConnectionError()
        with self.assertRaises(HTTPException) as err_ctx:
            await authenticate(creds)
        self.assertEqual(err_ctx.exception.status_code, HTTP_401_UNAUTHORIZED)
        self.assertIn("Authentication backend could not be reached", err_ctx.exception.detail)

        # Invalid credentials
        self.moonlanding_mock.side_effect = HTTPError(response=Response(status_code=401))
        with self.assertRaises(HTTPException) as err_ctx:
            await authenticate(creds)
        self.assertEqual(err_ctx.exception.status_code, HTTP_401_UNAUTHORIZED)
        self.assertIn("Invalid credentials", err_ctx.exception.detail)

        # Other error codes
        for err_code in [500, 403, 404]:
            with self.subTest("Error code: " + str(err_code)):
                self.moonlanding_mock.side_effect = HTTPError(response=Response(status_code=err_code))
                with self.assertRaises(HTTPException) as err_ctx:
                    await authenticate(creds)
                self.assertEqual(err_ctx.exception.status_code, HTTP_401_UNAUTHORIZED)
                self.assertIn("Error when authenticating", err_ctx.exception.detail)

    async def test_auth_guards_user(self):
        creds = HTTPAuthorizationCredentials(scheme="Bearer", credentials="api_faketoken")
        self.set_moonlanding_response()
        await authenticate(creds)
