from unittest import mock

import boto3
import pytest
from cryptography.hazmat.primitives.asymmetric import rsa

from moto import mock_aws
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID

grantee_principal = (
    f"arn:aws:iam::{ACCOUNT_ID}:role/service-role/tf-acc-test-7071877926602081451"
)


@mock_aws
def test_create_grant():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)

    resp = client.create_grant(
        KeyId=key_id,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT"],
        Name="testgrant",
    )
    assert "GrantId" in resp
    assert "GrantToken" in resp


@mock_aws
def test_list_grants():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)

    assert client.list_grants(KeyId=key_id)["Grants"] == []

    grant_id1 = client.create_grant(
        KeyId=key_id,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT"],
        Name="testgrant",
    )["GrantId"]

    grant_id2 = client.create_grant(
        KeyId=key_id,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT", "ENCRYPT"],
        Constraints={"EncryptionContextSubset": {"baz": "kaz", "foo": "bar"}},
    )["GrantId"]

    # List all
    grants = client.list_grants(KeyId=key_id)["Grants"]
    assert len(grants) == 2
    grant_1 = [grant for grant in grants if grant["GrantId"] == grant_id1][0]
    grant_2 = [grant for grant in grants if grant["GrantId"] == grant_id2][0]

    assert grant_1["KeyId"] == key_id
    assert grant_1["GrantId"] == grant_id1
    assert grant_1["Name"] == "testgrant"
    assert grant_1["GranteePrincipal"] == grantee_principal
    assert grant_1["Operations"] == ["DECRYPT"]

    assert grant_2["KeyId"] == key_id
    assert grant_2["GrantId"] == grant_id2
    assert "Name" not in grant_2
    assert grant_2["GranteePrincipal"] == grantee_principal
    assert grant_2["Operations"] == ["DECRYPT", "ENCRYPT"]
    assert grant_2["Constraints"] == {
        "EncryptionContextSubset": {"baz": "kaz", "foo": "bar"}
    }

    # List by grant_id
    grants = client.list_grants(KeyId=key_id, GrantId=grant_id2)["Grants"]
    assert len(grants) == 1
    assert grants[0]["GrantId"] == grant_id2

    # List by unknown grant_id
    grants = client.list_grants(KeyId=key_id, GrantId="unknown")["Grants"]
    assert len(grants) == 0


@mock_aws
def test_list_retirable_grants():
    client = boto3.client("kms", region_name="us-east-1")
    key_id1 = create_key(client)
    key_id2 = create_key(client)

    client.create_grant(
        KeyId=key_id1,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT"],
    )

    client.create_grant(
        KeyId=key_id1,
        GranteePrincipal=grantee_principal,
        RetiringPrincipal="sth else",
        Operations=["DECRYPT"],
    )

    client.create_grant(
        KeyId=key_id2,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT"],
    )

    grant2_key2 = client.create_grant(
        KeyId=key_id2,
        GranteePrincipal=grantee_principal,
        RetiringPrincipal="principal",
        Operations=["DECRYPT"],
    )["GrantId"]

    # List only the grants from the retiring principal
    grants = client.list_retirable_grants(RetiringPrincipal="principal")["Grants"]
    assert len(grants) == 1
    assert grants[0]["KeyId"] == key_id2
    assert grants[0]["GrantId"] == grant2_key2


@mock_aws
def test_revoke_grant():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)

    assert client.list_grants(KeyId=key_id)["Grants"] == []

    grant_id = client.create_grant(
        KeyId=key_id,
        GranteePrincipal=grantee_principal,
        Operations=["DECRYPT"],
        Name="testgrant",
    )["GrantId"]

    client.revoke_grant(KeyId=key_id, GrantId=grant_id)

    assert len(client.list_grants(KeyId=key_id)["Grants"]) == 0


@mock_aws
def test_revoke_grant_raises_when_grant_does_not_exist():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)
    not_existent_grant_id = "aabbccdd"

    with pytest.raises(client.exceptions.NotFoundException) as ex:
        client.revoke_grant(KeyId=key_id, GrantId=not_existent_grant_id)

    assert ex.value.response["Error"]["Code"] == "NotFoundException"
    assert (
        ex.value.response["Error"]["Message"]
        == f"Grant ID {not_existent_grant_id} not found"
    )


@mock_aws
def test_retire_grant_by_token():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)

    for idx in range(0, 3):
        grant_token = client.create_grant(
            KeyId=key_id,
            GranteePrincipal=grantee_principal,
            Operations=["DECRYPT"],
            Name=f"testgrant{idx}",
        )["GrantToken"]

    client.retire_grant(GrantToken=grant_token)

    assert len(client.list_grants(KeyId=key_id)["Grants"]) == 2


@mock_aws
def test_retire_grant_by_grant_id():
    client = boto3.client("kms", region_name="us-east-1")
    key_id = create_key(client)

    for idx in range(0, 3):
        grant_id = client.create_grant(
            KeyId=key_id,
            GranteePrincipal=grantee_principal,
            Operations=["DECRYPT"],
            Name=f"testgrant{idx}",
        )["GrantId"]

    client.retire_grant(KeyId=key_id, GrantId=grant_id)

    assert len(client.list_grants(KeyId=key_id)["Grants"]) == 2


def create_key(client):
    with mock.patch.object(rsa, "generate_private_key", return_value=""):
        return client.create_key(Policy="my policy")["KeyMetadata"]["KeyId"]
