import boto3
import pytest
from boto3.dynamodb.conditions import Key
from datetime import datetime
from botocore.exceptions import ClientError
from moto import mock_dynamodb
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID


@mock_dynamodb
def test_create_table():
    client = boto3.client("dynamodb", region_name="us-east-2")
    client.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[
            {"AttributeName": "id", "AttributeType": "S"},
            {"AttributeName": "gsi_col", "AttributeType": "S"},
        ],
        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
        GlobalSecondaryIndexes=[
            {
                "IndexName": "test_gsi",
                "KeySchema": [{"AttributeName": "gsi_col", "KeyType": "HASH"}],
                "Projection": {"ProjectionType": "ALL"},
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 1,
                    "WriteCapacityUnits": 1,
                },
            }
        ],
    )

    actual = client.describe_table(TableName="messages")["Table"]

    assert actual["AttributeDefinitions"] == [
        {"AttributeName": "id", "AttributeType": "S"},
        {"AttributeName": "gsi_col", "AttributeType": "S"},
    ]
    assert isinstance(actual["CreationDateTime"], datetime)
    assert actual["GlobalSecondaryIndexes"] == [
        {
            "IndexName": "test_gsi",
            "KeySchema": [{"AttributeName": "gsi_col", "KeyType": "HASH"}],
            "Projection": {"ProjectionType": "ALL"},
            "IndexStatus": "ACTIVE",
            "ProvisionedThroughput": {
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1,
            },
        }
    ]
    assert actual["LocalSecondaryIndexes"] == []
    assert actual["ProvisionedThroughput"] == {
        "NumberOfDecreasesToday": 0,
        "ReadCapacityUnits": 1,
        "WriteCapacityUnits": 1,
    }
    assert actual["TableSizeBytes"] == 0
    assert actual["TableName"] == "messages"
    assert actual["TableStatus"] == "ACTIVE"
    assert (
        actual["TableArn"] == f"arn:aws:dynamodb:us-east-2:{ACCOUNT_ID}:table/messages"
    )
    assert actual["KeySchema"] == [{"AttributeName": "id", "KeyType": "HASH"}]
    assert actual["ItemCount"] == 0


@mock_dynamodb
def test_delete_table():
    conn = boto3.client("dynamodb", region_name="us-west-2")
    conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )
    assert len(conn.list_tables()["TableNames"]) == 1

    conn.delete_table(TableName="messages")
    assert conn.list_tables()["TableNames"] == []

    with pytest.raises(ClientError) as ex:
        conn.delete_table(TableName="messages")

    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
    assert ex.value.response["Error"]["Message"] == "Requested resource not found"


@mock_dynamodb
def test_item_add_and_describe_and_update():
    conn = boto3.resource("dynamodb", region_name="us-west-2")
    table = conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )

    data = {
        "id": "LOLCat Forum",
        "Body": "http://url_to_lolcat.gif",
        "SentBy": "User A",
    }

    table.put_item(Item=data)
    returned_item = table.get_item(Key={"id": "LOLCat Forum"})
    assert "ConsumedCapacity" not in returned_item

    assert returned_item["Item"] == {
        "id": "LOLCat Forum",
        "Body": "http://url_to_lolcat.gif",
        "SentBy": "User A",
    }

    table.update_item(
        Key={"id": "LOLCat Forum"},
        UpdateExpression="SET SentBy=:user",
        ExpressionAttributeValues={":user": "User B"},
    )

    returned_item = table.get_item(Key={"id": "LOLCat Forum"})
    assert returned_item["Item"] == {
        "id": "LOLCat Forum",
        "Body": "http://url_to_lolcat.gif",
        "SentBy": "User B",
    }


@mock_dynamodb
def test_item_put_without_table():
    conn = boto3.client("dynamodb", region_name="us-west-2")

    with pytest.raises(ClientError) as ex:
        conn.put_item(
            TableName="messages",
            Item={
                "forum_name": {"S": "LOLCat Forum"},
                "Body": {"S": "http://url_to_lolcat.gif"},
                "SentBy": {"S": "User A"},
            },
        )

    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
    assert ex.value.response["Error"]["Message"] == "Requested resource not found"


@mock_dynamodb
def test_get_item_with_undeclared_table():
    conn = boto3.client("dynamodb", region_name="us-west-2")

    with pytest.raises(ClientError) as ex:
        conn.get_item(TableName="messages", Key={"forum_name": {"S": "LOLCat Forum"}})

    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
    assert ex.value.response["Error"]["Message"] == "Requested resource not found"


@mock_dynamodb
def test_delete_item():
    conn = boto3.resource("dynamodb", region_name="us-west-2")
    table = conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )

    item_data = {
        "id": "LOLCat Forum",
        "Body": "http://url_to_lolcat.gif",
        "SentBy": "User A",
        "ReceivedTime": "12/9/2011 11:36:03 PM",
    }
    table.put_item(Item=item_data)

    assert table.item_count == 1

    table.delete_item(Key={"id": "LOLCat Forum"})

    assert table.item_count == 0

    table.delete_item(Key={"id": "LOLCat Forum"})


@mock_dynamodb
def test_delete_item_with_undeclared_table():
    conn = boto3.client("dynamodb", region_name="us-west-2")

    with pytest.raises(ClientError) as ex:
        conn.delete_item(
            TableName="messages", Key={"forum_name": {"S": "LOLCat Forum"}}
        )

    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
    assert ex.value.response["Error"]["Message"] == "Requested resource not found"


@mock_dynamodb
def test_scan_with_undeclared_table():
    conn = boto3.client("dynamodb", region_name="us-west-2")

    with pytest.raises(ClientError) as ex:
        conn.scan(TableName="messages")

    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
    assert ex.value.response["Error"]["Message"] == "Requested resource not found"


@mock_dynamodb
def test_get_key_schema():
    conn = boto3.resource("dynamodb", region_name="us-west-2")
    table = conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )

    assert table.key_schema == [{"AttributeName": "id", "KeyType": "HASH"}]


@mock_dynamodb
def test_update_item_double_nested_remove():
    conn = boto3.client("dynamodb", region_name="us-east-1")
    conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
        BillingMode="PAY_PER_REQUEST",
    )

    item = {
        "username": {"S": "steve"},
        "Meta": {
            "M": {"Name": {"M": {"First": {"S": "Steve"}, "Last": {"S": "Urkel"}}}}
        },
    }
    conn.put_item(TableName="messages", Item=item)
    key_map = {"username": {"S": "steve"}}

    # Then remove the Meta.FullName field
    conn.update_item(
        TableName="messages",
        Key=key_map,
        UpdateExpression="REMOVE Meta.#N.#F",
        ExpressionAttributeNames={"#N": "Name", "#F": "First"},
    )

    returned_item = conn.get_item(TableName="messages", Key=key_map)
    expected_item = {
        "username": {"S": "steve"},
        "Meta": {"M": {"Name": {"M": {"Last": {"S": "Urkel"}}}}},
    }
    assert returned_item["Item"] == expected_item


@mock_dynamodb
def test_update_item_set():
    conn = boto3.resource("dynamodb", region_name="us-east-1")
    table = conn.create_table(
        TableName="messages",
        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
        BillingMode="PAY_PER_REQUEST",
    )

    data = {"username": "steve", "SentBy": "User A"}
    table.put_item(Item=data)
    key_map = {"username": "steve"}

    table.update_item(
        Key=key_map,
        UpdateExpression="SET foo=:bar, blah=:baz REMOVE SentBy",
        ExpressionAttributeValues={":bar": "bar", ":baz": "baz"},
    )

    returned_item = table.get_item(Key=key_map)["Item"]
    assert returned_item == {"username": "steve", "foo": "bar", "blah": "baz"}


@mock_dynamodb
def test_create_table__using_resource():
    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")

    table = dynamodb.create_table(
        TableName="users",
        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )
    assert table.name == "users"


def _create_user_table():
    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")

    dynamodb.create_table(
        TableName="users",
        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )
    return dynamodb.Table("users")


@mock_dynamodb
def test_conditions():
    table = _create_user_table()

    table.put_item(Item={"username": "johndoe"})
    table.put_item(Item={"username": "janedoe"})

    response = table.query(KeyConditionExpression=Key("username").eq("johndoe"))
    assert response["Count"] == 1
    assert response["Items"] == [{"username": "johndoe"}]


@mock_dynamodb
def test_put_item_conditions_pass():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.put_item(
        Item={"username": "johndoe", "foo": "baz"},
        Expected={"foo": {"ComparisonOperator": "EQ", "AttributeValueList": ["bar"]}},
    )
    final_item = table.get_item(Key={"username": "johndoe"})
    assert final_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_put_item_conditions_pass_because_expect_not_exists_by_compare_to_null():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.put_item(
        Item={"username": "johndoe", "foo": "baz"},
        Expected={"whatever": {"ComparisonOperator": "NULL"}},
    )
    final_item = table.get_item(Key={"username": "johndoe"})
    assert final_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_put_item_conditions_pass_because_expect_exists_by_compare_to_not_null():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.put_item(
        Item={"username": "johndoe", "foo": "baz"},
        Expected={"foo": {"ComparisonOperator": "NOT_NULL"}},
    )
    final_item = table.get_item(Key={"username": "johndoe"})
    assert final_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_put_item_conditions_fail():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    with pytest.raises(ClientError) as exc:
        table.put_item(
            Item={"username": "johndoe", "foo": "baz"},
            Expected={
                "foo": {"ComparisonOperator": "NE", "AttributeValueList": ["bar"]}
            },
        )
    err = exc.value.response["Error"]
    assert err["Code"] == "ConditionalCheckFailedException"


@mock_dynamodb
def test_update_item_conditions_fail():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "baz"})
    with pytest.raises(ClientError) as exc:
        table.update_item(
            Key={"username": "johndoe"},
            UpdateExpression="SET foo=:bar",
            Expected={"foo": {"Value": "bar"}},
            ExpressionAttributeValues={":bar": "bar"},
        )
    err = exc.value.response["Error"]
    assert err["Code"] == "ConditionalCheckFailedException"


@mock_dynamodb
def test_update_item_conditions_fail_because_expect_not_exists():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "baz"})
    with pytest.raises(ClientError) as exc:
        table.update_item(
            Key={"username": "johndoe"},
            UpdateExpression="SET foo=:bar",
            Expected={"foo": {"Exists": False}},
            ExpressionAttributeValues={":bar": "bar"},
        )
    err = exc.value.response["Error"]
    assert err["Code"] == "ConditionalCheckFailedException"


@mock_dynamodb
def test_update_item_conditions_fail_because_expect_not_exists_by_compare_to_null():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "baz"})
    with pytest.raises(ClientError) as exc:
        table.update_item(
            Key={"username": "johndoe"},
            UpdateExpression="SET foo=:bar",
            Expected={"foo": {"ComparisonOperator": "NULL"}},
            ExpressionAttributeValues={":bar": "bar"},
        )
    err = exc.value.response["Error"]
    assert err["Code"] == "ConditionalCheckFailedException"


@mock_dynamodb
def test_update_item_conditions_pass():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:baz",
        Expected={"foo": {"Value": "bar"}},
        ExpressionAttributeValues={":baz": "baz"},
    )
    returned_item = table.get_item(Key={"username": "johndoe"})
    assert returned_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_update_item_conditions_pass_because_expect_not_exists():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:baz",
        Expected={"whatever": {"Exists": False}},
        ExpressionAttributeValues={":baz": "baz"},
    )
    returned_item = table.get_item(Key={"username": "johndoe"})
    assert returned_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_update_item_conditions_pass_because_expect_not_exists_by_compare_to_null():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:baz",
        Expected={"whatever": {"ComparisonOperator": "NULL"}},
        ExpressionAttributeValues={":baz": "baz"},
    )
    returned_item = table.get_item(Key={"username": "johndoe"})
    assert returned_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_update_item_conditions_pass_because_expect_exists_by_compare_to_not_null():
    table = _create_user_table()
    table.put_item(Item={"username": "johndoe", "foo": "bar"})
    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:baz",
        Expected={"foo": {"ComparisonOperator": "NOT_NULL"}},
        ExpressionAttributeValues={":baz": "baz"},
    )
    returned_item = table.get_item(Key={"username": "johndoe"})
    assert returned_item["Item"]["foo"] == "baz"


@mock_dynamodb
def test_update_settype_item_with_conditions():
    class OrderedSet(set):
        """A set with predictable iteration order"""

        def __init__(self, values):
            super().__init__(values)
            self.__ordered_values = values

        def __iter__(self):
            return iter(self.__ordered_values)

    table = _create_user_table()
    table.put_item(Item={"username": "johndoe"})
    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:new_value",
        ExpressionAttributeValues={":new_value": OrderedSet(["hello", "world"])},
    )

    table.update_item(
        Key={"username": "johndoe"},
        UpdateExpression="SET foo=:new_value",
        ExpressionAttributeValues={":new_value": set(["baz"])},
        Expected={
            "foo": {
                "ComparisonOperator": "EQ",
                "AttributeValueList": [
                    OrderedSet(["world", "hello"])  # Opposite order to original
                ],
            }
        },
    )
    returned_item = table.get_item(Key={"username": "johndoe"})
    assert returned_item["Item"]["foo"] == set(["baz"])


@mock_dynamodb
def test_scan_pagination():
    table = _create_user_table()

    expected_usernames = [f"user{i}" for i in range(10)]
    for u in expected_usernames:
        table.put_item(Item={"username": u})

    page1 = table.scan(Limit=6)
    assert page1["Count"] == 6
    assert len(page1["Items"]) == 6

    page2 = table.scan(Limit=6, ExclusiveStartKey=page1["LastEvaluatedKey"])
    assert page2["Count"] == 4
    assert len(page2["Items"]) == 4
    assert "LastEvaluatedKey" not in page2

    results = page1["Items"] + page2["Items"]
    usernames = set([r["username"] for r in results])
    assert usernames == set(expected_usernames)


@mock_dynamodb
def test_scan_by_index():
    dynamodb = boto3.client("dynamodb", region_name="us-east-1")

    dynamodb.create_table(
        TableName="test",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[
            {"AttributeName": "id", "AttributeType": "S"},
            {"AttributeName": "gsi_col", "AttributeType": "S"},
        ],
        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
        GlobalSecondaryIndexes=[
            {
                "IndexName": "test_gsi",
                "KeySchema": [{"AttributeName": "gsi_col", "KeyType": "HASH"}],
                "Projection": {"ProjectionType": "ALL"},
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 1,
                    "WriteCapacityUnits": 1,
                },
            }
        ],
    )

    dynamodb.put_item(
        TableName="test",
        Item={"id": {"S": "1"}, "col1": {"S": "val1"}, "gsi_col": {"S": "gsi_val1"}},
    )

    dynamodb.put_item(
        TableName="test",
        Item={"id": {"S": "2"}, "col1": {"S": "val2"}, "gsi_col": {"S": "gsi_val2"}},
    )

    dynamodb.put_item(TableName="test", Item={"id": {"S": "3"}, "col1": {"S": "val3"}})

    res = dynamodb.scan(TableName="test")
    assert res["Count"] == 3
    assert len(res["Items"]) == 3

    res = dynamodb.scan(TableName="test", IndexName="test_gsi")
    assert res["Count"] == 2
    assert len(res["Items"]) == 2

    res = dynamodb.scan(TableName="test", IndexName="test_gsi", Limit=1)
    assert res["Count"] == 1
    assert len(res["Items"]) == 1
    last_eval_key = res["LastEvaluatedKey"]
    assert last_eval_key["id"]["S"] == "1"
    assert last_eval_key["gsi_col"]["S"] == "gsi_val1"
