# Copyright (c) 2020 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com

import json
from abc import abstractmethod
from collections import abc
from pprint import pformat
from typing import Any, Mapping, MutableMapping, Tuple
from uuid import uuid4

import boto3
import czc.smoke.token as token
import pyfaaster.aws.tools as faaster_aws_tools
import pytest
import requests
from czc.api.v2.constants import CLOUDZERO_TEST_KEY_HEADER
from czc.constants import FEATURE_NAME
from czc.internal_api.client import RemoteApiError
from czc.smoke import api_key
from toolz.dicttoolz import get_in, keyfilter, valmap
from voluptuous import ALLOW_EXTRA, Schema

lba = boto3.client('lambda')
logger = faaster_aws_tools.setup_logging(FEATURE_NAME)


class AuthInfo():
    _namespace: str

    def __init__(self, namespace: str) -> None:
        self._namespace = namespace

    @abstractmethod
    def fetch_auth_info(self) -> Tuple[str, str, dict]:
        ...


class TokenApiAuth(AuthInfo):
    _credentials: Any
    _user: str
    _user_id: str
    _token: Any

    @property
    def user_id(self):
        return self._user_id

    @property
    def token(self):
        return self._token

    def __init__(self, namespace, username, credentials) -> None:
        super().__init__(namespace)
        self._credentials = credentials
        self._username = username

    def fetch_auth_info(self) -> Tuple[str, str, dict]:
        user = (self._username, self._credentials['password'])
        self._token = token.fetch(self._namespace, self._credentials['tenant'], user)
        org_id, self._user_id = token.token_to_ids(self._credentials['tenant'][0], self._token)
        return org_id, f'Bearer {self._token}', {'userId': self._user_id}


class ApiKeyAuth(AuthInfo):
    _org_id: str

    def __init__(self, namespace, org_id) -> None:
        super().__init__(namespace)
        self._org_id = org_id

    def fetch_auth_info(self) -> Tuple[str, str, dict]:
        return self._org_id, api_key.fetch(self._namespace, self._org_id), {}


class IapiAuth(AuthInfo):
    _org_id: str

    def __init__(self, namespace, org_id) -> None:
        super().__init__(namespace)
        self._org_id = org_id

    def fetch_auth_info(self) -> Tuple[str, str, dict]:
        return self._org_id, '', {'Namespace': self._namespace, 'FeatureName': FEATURE_NAME}


class _ApiTestRunner():
    _org_id: str
    _auth_key: Any
    _namespace: str
    _configuration: Any
    _state: Any
    _resolved_test: Mapping
    _path: str
    _url: str
    _path_parameters: Mapping
    _method: str
    _request_kwargs: Mapping
    _result: Any

    @property
    def namespace(self):
        return self._namespace

    @property
    def org_id(self):
        return self._org_id

    @property
    def auth_key(self):
        return self._auth_key

    @property
    def state(self):
        return self._state

    def __init__(self, namespace, configuration, test, public_url, auth_info: AuthInfo, state=None) -> None:
        super().__init__()
        if namespace in test.get('exclude_namespaces', []):
            pytest.skip(f'Not running test for namespace {namespace}')

        self._namespace = namespace
        self._configuration = configuration
        self._org_id, self._auth_key, additional_path_params = auth_info.fetch_auth_info()
        self._state = state

        logger.debug(pformat(configuration))
        logger.debug(pformat(test))

        self._resolved_test = self._resolve(test)

        self._path = get_in(['endpoints', self._resolved_test['endpoint'], 'path'], configuration) or ''
        self._result = self._resolved_test['result']

        path_parameters = {
            'organizationId': self._org_id,
            **(additional_path_params or {}),
            **self._resolved_test.get('pathParameters', {})
        }

        self._url = f'{public_url.rstrip("/")}/{self._path.format(**path_parameters).lstrip("/")}'
        self._method = get_in(['endpoints', self._resolved_test['endpoint'], 'method'], configuration) or ''
        self._request_kwargs = keyfilter(lambda x: x in {'json', 'body', 'params', 'files'}, self._resolved_test)

    def _resolve(self, test):
        def f(x):
            if isinstance(x, abc.Mapping):
                return self._resolve(x)
            elif callable(x) and ('voluptuous' not in str(type(x))):
                return x(self)
            return x
        return valmap(f, test)

    def _validate_response(self, response):
        logger.debug(f'response_dict {pformat(response)}')
        logger.debug(f'result {pformat(self._result)}')
        s = self._result if isinstance(self._result, Schema) else Schema(self._result, extra=ALLOW_EXTRA)
        s(response)


class _HttpApiTestRunner(_ApiTestRunner):
    _cz_test_key: str
    _additional_headers: dict

    def __init__(self, namespace, configuration, test, public_url, auth_info: AuthInfo, state=None, use_test_key=True, cz_test_key=None, additional_headers=None) -> None:
        super().__init__(namespace, configuration, test, public_url, auth_info, state)
        self._additional_headers = additional_headers or {}
        self._cz_test_key = (cz_test_key or f'smoketest-{uuid4()}') if use_test_key else ''

    def run(self):
        headers = {
            'Authorization': self._auth_key,
            'Origin': 'https://smoke-test.cloudzero.com',
            **({CLOUDZERO_TEST_KEY_HEADER: self._cz_test_key} if self._cz_test_key else {}),
            **self._resolved_test.get('headers', {}),
            **self._additional_headers
        }

        logger.debug(f'Sending {self._method} request {self._url} with {pformat(self._request_kwargs)}')
        response_body = {}
        response = requests.request(self._method, self._url, headers=headers, **self._request_kwargs, timeout=20)
        try:
            response_body = response.json()
        except Exception:
            response_body = response.text
        response_dict = {
            'statusCode': response.status_code,
            'body': response_body,
        }

        self._validate_response(response_dict)
        return response_dict


class _PublicV2ApiTestRunner(_HttpApiTestRunner):
    def __init__(self, namespace, configuration, test, public_url, auth_info: AuthInfo, state=None, use_test_key=True, cz_test_key=None) -> None:
        super().__init__(namespace, configuration, test, public_url, auth_info, state, use_test_key, cz_test_key)


class PublicV2ApiKeyTestRunner(_PublicV2ApiTestRunner):
    def __init__(self, namespace, org_id, configuration, test, public_url, state=None, use_test_key=True, cz_test_key=None) -> None:
        super().__init__(namespace, configuration, test, public_url, ApiKeyAuth(namespace, org_id), state, use_test_key, cz_test_key)


class PublicV2ApiTokenTestRunner(_PublicV2ApiTestRunner):
    _auth_info: TokenApiAuth

    @property
    def user_id(self):
        return self._auth_info.user_id

    @property
    def token(self):
        return self._auth_info.token

    def __init__(self, namespace, credentials, configuration, test, public_url, state=None, use_test_key=True, cz_test_key=None) -> None:
        self._auth_info = TokenApiAuth(namespace, test['user'], credentials)
        super().__init__(namespace, configuration, test, public_url, self._auth_info, state, use_test_key, cz_test_key)


class PublicApiTestRunner(_HttpApiTestRunner):
    _auth_info: TokenApiAuth

    @property
    def user_id(self):
        return self._auth_info.user_id

    @property
    def token(self):
        return self._auth_info.token

    def __init__(self, namespace, credentials, configuration, test, public_url, state=None, use_test_key=True, cz_test_key=None) -> None:
        self._auth_info = TokenApiAuth(namespace, test['user'], credentials)
        super().__init__(namespace, configuration, test, public_url, self._auth_info, state, use_test_key, cz_test_key)


class IapiClientTestRunner(_ApiTestRunner):
    _request_kwargs: MutableMapping[str, Any]

    def __init__(self, namespace, organization_id, configuration, test, state=None) -> None:
        super().__init__(namespace, configuration, test, '', auth_info=IapiAuth(namespace, organization_id), state=state)
        self._request_kwargs = self._resolved_test['params']

    def run(self, test_call):
        if 'cz_organization_id' in self._request_kwargs:
            self._request_kwargs['cz_organization_id'] = self._request_kwargs['cz_organization_id'].format(cz_organization_id=self._org_id)

        logger.debug(f'Calling lambda {self._path} with {pformat(self._request_kwargs)}')
        try:
            invoke_response = test_call(self.org_id, self._request_kwargs)
        except RemoteApiError as api_error:
            invoke_response = {
                'error': api_error.error
            }
        self._validate_response(invoke_response)
        return invoke_response


class PrivateApiTestRunner(_ApiTestRunner):
    _request_kwargs: MutableMapping

    def __init__(self, namespace, organization_id, configuration, test, state=None) -> None:
        super().__init__(namespace, configuration, test, '', auth_info=IapiAuth(namespace, organization_id), state=state)
        self._request_kwargs = self._resolved_test['params']

    def run(self):
        if 'cz_organization_id' in self._request_kwargs:
            self._request_kwargs['cz_organization_id'] = self._request_kwargs['cz_organization_id'].format(cz_organization_id=self._org_id)

        logger.debug(f'Calling lambda {self._path} with {pformat(self._request_kwargs)}')

        invoke_response = lba.invoke(
            FunctionName=self._path.format(Namespace=self._namespace, FeatureName=FEATURE_NAME),
            InvocationType='RequestResponse',
            Payload=json.dumps(self._request_kwargs))

        response = {
            'statusCode': invoke_response['StatusCode'],
            'body': json.loads(invoke_response['Payload'].read().decode("utf-8"))
        }

        self._validate_response(response)
        return response
