import json
from typing import Optional

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_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_client_error(self):
        # Set expected values
        expected_key = "token.key"
        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 GetToken() ClientError
        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: Optional[str] = None) -> ClientError:
        operation = operation or "test operation"
        return ClientError({"Error": {"Code": code}}, operation)
