from chia_rs import Coin
from hashlib import sha256
import copy
import pytest
from chia_rs.sized_ints import uint64
from chia_rs.sized_bytes import bytes32

parent_coin = b"---foo---                       "
puzzle_hash = b"---bar---                       "
puzzle_hash2 = b"---bar--- 2                     "


def test_coin_name() -> None:

    c = Coin(parent_coin, puzzle_hash, uint64(0))
    assert c.name() == sha256(parent_coin + puzzle_hash).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(1))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([1])).digest()

    # 0xFF prefix
    c = Coin(parent_coin, puzzle_hash, uint64(0xFF))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([0, 0xFF])).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFF))
    assert (
        c.name() == sha256(parent_coin + puzzle_hash + bytes([0, 0xFF, 0xFF])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFF))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0, 0xFF, 0xFF, 0xFF])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin
            + puzzle_hash
            + bytes([0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0xFFFFFFFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin
            + puzzle_hash
            + bytes([0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    # 0x7F prefix
    c = Coin(parent_coin, puzzle_hash, uint64(0x7F))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([0x7F])).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFF))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([0x7F, 0xFF])).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFF))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0x7F, 0xFF, 0xFF])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFFFF))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0x7F, 0xFF, 0xFF, 0xFF])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0x7F, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin
            + puzzle_hash
            + bytes([0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )
    c = Coin(parent_coin, puzzle_hash, uint64(0x7FFFFFFFFFFFFFFF))
    assert (
        c.name()
        == sha256(
            parent_coin
            + puzzle_hash
            + bytes([0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        ).digest()
    )

    # 0x80 prefix
    c = Coin(parent_coin, puzzle_hash, uint64(0x80))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([0, 0x80])).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(0x8000))
    assert c.name() == sha256(parent_coin + puzzle_hash + bytes([0, 0x80, 0])).digest()

    c = Coin(parent_coin, puzzle_hash, uint64(0x800000))
    assert (
        c.name() == sha256(parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x80000000))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0, 0])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x8000000000))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0, 0, 0])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x800000000000))
    assert (
        c.name()
        == sha256(parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0, 0, 0, 0])).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x80000000000000))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0, 0, 0, 0, 0])
        ).digest()
    )

    c = Coin(parent_coin, puzzle_hash, uint64(0x8000000000000000))
    assert (
        c.name()
        == sha256(
            parent_coin + puzzle_hash + bytes([0, 0x80, 0, 0, 0, 0, 0, 0, 0])
        ).digest()
    )


def test_coin_copy() -> None:

    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    c2 = copy.copy(c1)

    assert c1 == c2
    assert c1 is not c2


def test_coin_deepcopy() -> None:

    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    c2 = copy.deepcopy(c1)

    assert c1 == c2
    assert c1 is not c2


def coin_json_roundtrip(c: Coin) -> bool:
    d = c.to_json_dict()
    c2 = Coin.from_json_dict(d)
    return c == c2 and c.name() == c2.name()


def test_coin_to_json() -> None:

    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    assert c1.to_json_dict() == {
        "parent_coin_info": "0x" + parent_coin.hex(),
        "puzzle_hash": "0x" + puzzle_hash.hex(),
        "amount": 1000000,
    }
    assert coin_json_roundtrip(c1)

    c2 = Coin(parent_coin, puzzle_hash2, uint64(0))
    assert c2.to_json_dict() == {
        "parent_coin_info": "0x" + parent_coin.hex(),
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 0,
    }
    assert coin_json_roundtrip(c2)

    c3 = Coin(parent_coin, puzzle_hash2, uint64(0xFFFFFFFFFFFFFFFF))
    assert c3.to_json_dict() == {
        "parent_coin_info": "0x" + parent_coin.hex(),
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 0xFFFFFFFFFFFFFFFF,
    }
    assert coin_json_roundtrip(c3)


def test_coin_from_json() -> None:

    c = {
        "parent_coin_info": "0x" + parent_coin.hex(),
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    assert Coin.from_json_dict(c) == Coin(parent_coin, puzzle_hash2, uint64(12345678))


def test_coin_from_json_upper_hex() -> None:

    c = {
        "parent_coin_info": "0x" + parent_coin.hex().upper(),
        "puzzle_hash": "0x" + puzzle_hash2.hex().upper(),
        "amount": 12345678,
    }
    assert Coin.from_json_dict(c) == Coin(parent_coin, puzzle_hash2, uint64(12345678))


def test_coin_from_json_lower_hex() -> None:

    c = {
        "parent_coin_info": "0x" + parent_coin.hex().lower(),
        "puzzle_hash": "0x" + puzzle_hash2.hex().lower(),
        "amount": 12345678,
    }
    assert Coin.from_json_dict(c) == Coin(parent_coin, puzzle_hash2, uint64(12345678))


def test_coin_from_json_invalid_hex_prefix() -> None:

    c = {
        # this field is missing "0x"-prefix
        "parent_coin_info": parent_coin.hex(),
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(ValueError, match="bytes object is expected to start with 0x"):
        Coin.from_json_dict(c)


def test_coin_from_json_invalid_hex_prefix2() -> None:

    c = {
        "parent_coin_info": "0x" + parent_coin.hex(),
        # this field is missing "0x"-prefix
        "puzzle_hash": puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(ValueError, match="bytes object is expected to start with 0x"):
        Coin.from_json_dict(c)


def test_coin_from_json_hex_digit() -> None:

    c = {
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    assert Coin.from_json_dict(c) == Coin(
        bytes.fromhex(
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
        ),
        puzzle_hash2,
        uint64(12345678),
    )


def test_coin_from_json_invalid_hex_digit() -> None:

    c = {
        # this field has an invalid hex digit
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdefg123456789abcdef",
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(ValueError, match="invalid hex"):
        Coin.from_json_dict(c)


def test_coin_from_json_invalid_hex_len() -> None:

    c = {
        # this field has an invalid length (missing one character)
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde",
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(ValueError, match="invalid hex"):
        Coin.from_json_dict(c)


def test_coin_from_json_invalid_hex_len2() -> None:

    c = {
        # this field has an invalid length (missing two character)
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(ValueError, match="invalid length 31 expected 32"):
        Coin.from_json_dict(c)


def test_coin_from_json_missing_field1() -> None:

    c = {
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
        "amount": 12345678,
    }
    with pytest.raises(KeyError, match="parent_coin_info"):
        Coin.from_json_dict(c)


def test_coin_from_json_missing_field2() -> None:

    c = {
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "amount": 12345678,
    }
    with pytest.raises(KeyError, match="puzzle_hash"):
        Coin.from_json_dict(c)


def test_coin_from_json_missing_field3() -> None:

    c = {
        "parent_coin_info": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "puzzle_hash": "0x" + puzzle_hash2.hex(),
    }
    with pytest.raises(KeyError, match="amount"):
        Coin.from_json_dict(c)


def test_coin_hash() -> None:

    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    c2 = Coin(parent_coin, puzzle_hash2, uint64(1000000))
    c3 = Coin(parent_coin, puzzle_hash, uint64(2000000))
    c4 = Coin(parent_coin, puzzle_hash, uint64(1000000))

    assert hash(c1) != hash(c2)
    assert hash(c1) != hash(c3)
    assert hash(c2) != hash(c3)

    assert hash(c1) == hash(c4)
    assert type(hash(c1)) is int


def test_coin_fields() -> None:

    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    assert c1.parent_coin_info == parent_coin
    assert c1.puzzle_hash == puzzle_hash
    assert c1.amount == 1000000


def test_coin_print() -> None:
    c1 = Coin(parent_coin, puzzle_hash, uint64(1000000))
    assert type(c1.name()) is bytes32
    assert (
        f"{c1.name()}"
        == "e1838c5c7ebb472e310600ce9c03c09b5e4bb77dde53f3427f6e8cc67dede32d"
    )
