import json
import pytest

from botocore.exceptions import ClientError
from psi_utils.secrets import MalformedSecretException, SecretsManager
from psi_utils.unittest_mock import MockApi, ANY_NOT_NONE


class TestSecrets_UnitTests:

    @pytest.mark.unit
    def test_encrypt(self):
        # Set expected values
        expected_username = 'user'
        expected_password = 'password'
        expected_alias = 'alias'

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'encrypt',
            KeyId=expected_alias,
            Plaintext=expected_password
        ).return_val({'CiphertextBlob': 'value'.encode('utf-8')})
        mock_session = MockApi()
        mock_session.on('client', 'kms').return_val(mock_client)

        # Test Encrypt()
        sm = SecretsManager(session=mock_session)
        username, encrypted = sm.Encrypt(expected_username, expected_password, expected_alias)
        assert username == expected_username
        assert encrypted == b'dmFsdWU='
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_decrypt(self):
        # Set expected values
        expected_secret = 'dmFsdWU='

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'decrypt',
            CiphertextBlob=b'value'
        ).return_val({'Plaintext': 'value2'})
        mock_session = MockApi()
        mock_session.on('client', 'kms').return_val(mock_client)

        # Test Decrypt()
        sm = SecretsManager(session=mock_session)
        plaintext = sm.Decrypt(expected_secret)
        assert plaintext == 'value2'
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_pushsecret(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_username = 'user'
        expected_password = b'password'
        expected_secret_string = json.dumps({
            'username': expected_username,
            'password': 'password'
        })
        expected_response = 1

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'put_secret_value',
            SecretId=expected_key,
            ClientRequestToken=ANY_NOT_NONE,
            SecretString=expected_secret_string
        ).return_val(expected_response)
        mock_session = MockApi()
        mock_session.on('client', 'secretsmanager', region_name='us-east-1').return_val(mock_client)

        # Test PushSecret()
        sm = SecretsManager(session=mock_session)
        resp = sm.PushSecret(expected_key, expected_username, expected_password)
        assert resp == expected_response
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_username = 'user'
        expected_password_b64 = 'dmFsdWU='
        expected_resp = {
            'SecretString': json.dumps({
                'username': expected_username,
                'password': expected_password_b64
            })
        }

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val(expected_resp)
        mock_kms_client = MockApi()
        mock_kms_client.on(
            'decrypt',
            CiphertextBlob=b'value'
        ).return_val({'Plaintext': 'value2'})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)
        mock_session.on('client', 'kms').return_val(mock_kms_client)

        # Test GetCredentials()
        sm = SecretsManager(session=mock_session)
        username, password = sm.GetCredentials(expected_key, double_decrypt_password=True)
        assert username == expected_username
        assert password == 'value2'
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials_no_username(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_password_b64 = 'dmFsdWU='
        expected_resp = {
            'SecretString': json.dumps({
                'password': expected_password_b64
            })
        }

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val(expected_resp)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetCredentials() no username
        sm = SecretsManager(session=mock_session)
        with pytest.raises(MalformedSecretException) as e_info:
            sm.GetCredentials(expected_key)
        assert str(e_info.value) == "secret does not contain a 'username' field"
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials_no_password(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_resp = {
            'SecretString': json.dumps({
                'username': 'user'
            })
        }

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val(expected_resp)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetCredentials() no username
        sm = SecretsManager(session=mock_session)
        with pytest.raises(MalformedSecretException) as e_info:
            sm.GetCredentials(expected_key)
        assert str(e_info.value) == "secret does not contain a 'password' field"
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials_DecryptionFailureException(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_error = self.get_client_error(code='DecryptionFailureException')

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).raise_exception(expected_error)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetCredentials() DecryptionFailureException
        sm = SecretsManager(session=mock_session)
        with pytest.raises(ClientError) as e_info:
            sm.GetCredentials(expected_key)
        assert e_info.value == expected_error
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials_client_error(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_error = self.get_client_error(code='UnknownCode')

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).raise_exception(expected_error)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetCredentials() ClientError
        sm = SecretsManager(session=mock_session)
        with pytest.raises(ClientError) as e_info:
            sm.GetCredentials(expected_key)
        assert e_info.value == expected_error
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getcredentials_exception(self):
        # Set expected values
        expected_key = 'secret.id'
        expected_error = Exception('test')

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).raise_exception(expected_error)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetCredentials() Exception
        sm = SecretsManager(session=mock_session)
        with pytest.raises(Exception) as e_info:
            sm.GetCredentials(expected_key)
        assert e_info.value == expected_error
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getparameter(self):
        # Set expected values
        expected_param_name = 'param.name'
        expected_param = 'param.value'

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_parameter',
            Name=expected_param_name,
            WithDecryption=True
        ).return_val({'Parameter': {'Value': expected_param}})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='ssm',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetParameter()
        sm = SecretsManager(session=mock_session)
        assert sm.GetParameter(expected_param_name) == expected_param
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getparameter_missing_value(self):
        # Set expected values
        expected_param_name = 'param.name'

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_parameter',
            Name=expected_param_name,
            WithDecryption=True
        ).return_val({})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='ssm',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetParameter()
        sm = SecretsManager(session=mock_session)
        with pytest.raises(Exception) as e_info:
            sm.GetParameter(expected_param_name)
        assert str(e_info.value) == 'Parameter value not found in response'
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getapikey(self):
        # Set expected values
        expected_username = 'user.name@pearson.com'
        expected_result = {'name': expected_username}
        expected_iterator = [{'items': [expected_result]}]

        # Set up mocks
        mock_paginator = MockApi()
        mock_paginator.on(
            'paginate',
            PaginationConfig={'PageSize': 10},
            includeValues=True
        ).return_val(expected_iterator)
        mock_client = MockApi()
        mock_client.on(
            'get_paginator',
            'get_api_keys'
        ).return_val(mock_paginator)
        mock_session = MockApi()
        mock_session.on(
            'client',
            'apigateway',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetApiKey()
        sm = SecretsManager(session=mock_session)
        assert sm.GetApiKey(expected_username) == expected_result
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_getapikey_no_match(self):
        # Set expected values
        expected_iterator = [{'items': []}]

        # Set up mocks
        mock_paginator = MockApi()
        mock_paginator.on(
            'paginate',
            PaginationConfig={'PageSize': 10},
            includeValues=True
        ).return_val(expected_iterator)
        mock_client = MockApi()
        mock_client.on(
            'get_paginator',
            'get_api_keys'
        ).return_val(mock_paginator)
        mock_session = MockApi()
        mock_session.on(
            'client',
            'apigateway',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetApiKey() no matching username
        sm = SecretsManager(session=mock_session)
        with pytest.raises(MalformedSecretException) as e_info:
            sm.GetApiKey('user.name@pearson.com')
        assert str(e_info.value).startswith('Unable to find API key')
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_gettoken(self):
        # Set expected values
        expected_key = 'token.key'
        expected_token = 'token.value'
        expected_secret_json = json.dumps({'token': expected_token})

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val({'SecretString': expected_secret_json})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetToken()
        sm = SecretsManager(session=mock_session)
        assert sm.GetToken(expected_key) == expected_token
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_gettoken_no_token(self):
        # Set expected values
        expected_key = 'token.key'
        expected_secret_json = '{}'

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val({'SecretString': expected_secret_json})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetToken() no token
        sm = SecretsManager(session=mock_session)
        with pytest.raises(MalformedSecretException) as e_info:
            sm.GetToken(expected_key)
        assert str(e_info.value) == "secret does not contain a 'token' field"
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_gettoken_exception(self):
        # Set expected values
        expected_key = 'token.key'
        expected_error = Exception('test')

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).raise_exception(expected_error)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetToken() no token
        sm = SecretsManager(session=mock_session)
        with pytest.raises(Exception) as e_info:
            sm.GetToken(expected_key)
        assert e_info.value == expected_error
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_get_secret(self):
        # Set expected values
        expected_key = 'secret.key'
        expected_secret = {'key': 'value'}
        expected_secret_json = json.dumps(expected_secret)

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val({'SecretString': expected_secret_json})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetSecret()
        sm = SecretsManager(session=mock_session)
        assert sm.GetSecret(expected_key) == expected_secret
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_get_secret_no_secret_string(self):
        # Set expected values
        expected_key = 'secret.key'

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).return_val({})
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetSecret() no secret string
        sm = SecretsManager(session=mock_session)
        with pytest.raises(MalformedSecretException) as e_info:
            sm.GetSecret(expected_key)
        assert str(e_info.value) == 'secret does not contain a SecretString'
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_get_secret_exception(self):
        # Set expected values
        expected_key = 'secret.key'
        expected_error = Exception('test')

        # Set up mocks
        mock_client = MockApi()
        mock_client.on(
            'get_secret_value',
            SecretId=expected_key
        ).raise_exception(expected_error)
        mock_session = MockApi()
        mock_session.on(
            'client',
            service_name='secretsmanager',
            region_name='us-east-1'
        ).return_val(mock_client)

        # Test GetSecret() Exception
        sm = SecretsManager(session=mock_session)
        with pytest.raises(Exception) as e_info:
            sm.GetSecret(expected_key)
        assert e_info.value == expected_error
        mock_client.assert_expectations()
        mock_session.assert_expectations()

    @pytest.mark.unit
    def test_get_item(self):
        sm = SecretsManager(session=MockApi())
        assert sm['_xray_capture'] is False

    @pytest.mark.unit
    def test_repr(self):
        sm = SecretsManager(session=MockApi())
        assert str(sm) == "<class 'psi_utils.secrets.SecretsManager'>"
    
    def get_client_error(self, code: str, operation: str = None) -> ClientError:
        operation = operation or 'test operation'
        return ClientError({'Error': {'Code': code}}, operation)