import random

from bwsi_grader.errors import StudentError

__seed__ = ["cardclass", "deckclass"]


def grade_card(StudentCard):
    from hashlib import sha224

    from bwsi_grader.print import print_passed

    def card_repr(rank, suit):
        _rank_to_str = {11: "Jack", 12: "Queen", 13: "King", 14: "Ace"}
        _suit_to_str = {"C": "Clubs", "H": "Hearts", "S": "Spades", "D": "Diamonds"}

        rank = _rank_to_str[rank] if rank in _rank_to_str else str(rank)
        return f"{rank} of {_suit_to_str[suit]}"

    # ensure that we can create a card
    try:
        c = StudentCard(4, "H")
    except (NameError, TypeError):
        raise AssertionError('Could not create card `Card(4, "H")`')

    # ensure there is a rank and a suit, and that they are correctly set
    try:
        c.rank
    except AttributeError:
        raise AttributeError(
            'Could not access the `rank` attribute after creating `Card(4, "H")`'
        )

    try:
        c.suit
    except AttributeError:
        raise AttributeError(
            'Could not access the `suit` attribute after creating `Card(4, "H")`'
        )

    assert (
        c.rank == 4
    ), f'The `rank` attribute did not match. Created card with `Card(4, "H")`. Expected 4; got {c.rank}'

    assert (
        c.suit == "H"
    ), f'The `suit`attribute did not match. Created card with `Card(4, "H")`. Expected "H"; got {c.suit}'

    assert (
        str(c).lower() == card_repr(4, "H").lower()
    ), f'__repr__ did not match after being lower-cased; expected {card_repr(4, "H").lower()} got {str(c).lower()}'

    try:
        c = StudentCard(13, "S")
    except (NameError, TypeError):
        raise AssertionError('Could not create card `Card(13, "S")`')

    assert (
        c.rank == 13
    ), f'The `rank` attribute did not match. Created card with `Card(13, "S")`. Expected 13; got {c.rank}'

    assert (
        c.suit == "S"
    ), f'The `suit`attribute did not match. Created card with `Card(13, "S")`. Expected "S"; got {c.suit}'

    assert (
        str(c).lower() == card_repr(13, "S").lower()
    ), f'__repr__ did not match after being lower-cased; expected {card_repr(13, "S").lower()} got {str(c).lower()}'

    for _ in range(100):
        rank1 = random.randint(2, 14)
        suit = ("C", "H", "S", "D")[random.randint(0, 3)]
        student_card1 = StudentCard(rank1, suit)
        c1 = card_repr(rank1, suit)
        assert (
            str(student_card1).lower() == c1.lower()
        ), f"__repr__ did not match after being lower-cased; expected {c1.lower()} got {str(student_card1).lower()}"

        rank2 = random.randint(2, 14)
        suit = ("C", "H", "S", "D")[random.randint(0, 3)]
        student_card2 = StudentCard(rank2, suit)
        c2 = card_repr(rank2, suit)
        assert (
            str(student_card2).lower() == c2.lower()
        ), f"__repr__ did not match after being lower-cased; expected {c2.lower()} got {str(student_card2).lower()}"

        try:
            assert (student_card1 < student_card2) == (rank1 < rank2)
        except AssertionError:
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            raise AssertionError(
                f"Testing {c1} < {c2}. Expected {rank1 < rank2}; got {student_card1 < student_card2}"
            )

        try:
            assert (student_card1 > student_card2) == (rank1 > rank2)
        except AssertionError:
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            raise AssertionError(
                f"Testing {c1} > {c2}. Expected {rank1 > rank2}; got {student_card1 > student_card2}"
            )

        try:
            assert (student_card1 == student_card2) == (rank1 == rank2)
        except AssertionError:
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            raise AssertionError(
                f"Testing {c1} == {c2}. Expected {rank1 == rank2}; got {student_card1 == student_card2}"
            )

        try:
            assert (student_card1 <= student_card2) == (rank1 <= rank2)
        except AssertionError:
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            raise AssertionError(
                f"Testing {c1} <= {c2}. Expected {rank1 <= rank2}; got {student_card1 <= student_card2}"
            )

        try:
            assert (student_card1 >= student_card2) == (rank1 >= rank2)
        except AssertionError:
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            # pad
            raise AssertionError(
                f"Testing {c1} >= {c2}. Expected {rank1 >= rank2}; got {student_card1 >= student_card2}"
            )

    print_passed(sha224(str.encode(__seed__[0])).hexdigest())


def grade_deck(StudentDeck):
    import random
    from hashlib import sha224

    from bwsi_grader.print import print_passed

    # ensure that we can create a deck
    try:
        deck = StudentDeck()
    except NameError:
        raise AssertionError(
            "Could not create deck by calling `Deck()`. Is `shuffled` optional?"
        )

    # ensure we can create a deck with a shuffled parameter
    try:
        deck = StudentDeck(True)
    except (NameError, TypeError):
        raise AssertionError(
            "Could not create deck with shuffled=True by calling `Deck(True)`"
        )

    # ensure that a deck has a shuffled parameter
    try:
        deck.shuffled
    except AttributeError:
        raise AttributeError("Could not access the `shuffled` parameter of `Deck`")

    # ensure that a shuffle function exists
    try:
        deck.shuffle()
    except AttributeError:
        raise AttributeError("Could not call `shuffle()` on `Deck`")

    # ensure that a deal_card function exists
    try:
        deck.deal_card()
    except AttributeError:
        raise AttributeError("Could not call `deal_card()` on `Deck`")

    # ensure that a reset function exists
    try:
        deck.reset()
    except AttributeError:
        raise AttributeError("Could not call `reset()` on `Deck`")

    # make sure the repr matches
    deck = str(StudentDeck()).lower()
    assert (
        deck == "deck(dealt 0, shuffled=false)"
    ), f'__repr__ did not match; expected "deck(dealt 0, shuffled=False)" got {deck}'

    # make sure the deck deals cards
    deck = StudentDeck()
    try:
        card = deck.deal_card()
        card.rank
        card.suit
    except AttributeError:
        raise AttributeError(
            f"The Deck should deal Card-instances. Dealt an object of type: {type(card)}"
            f"\nTo reproduce this error, call:"
            f"\ndeck = Deck()"
            f"\ncard = deck.deal_card()"
            f"\ncard.rank"
            f"\ncard.suit"
        )

    # make sure the deck has 52 cards
    deck = StudentDeck()
    try:
        for _ in range(52):
            card = deck.deal_card()
            card.rank
            card.suit
    except Exception as e:
        raise StudentError(
            f"Trying to deal 52 cards produced the following error:"
            f"\n\t{type(e).__name__}:{e}"
        )

    try:
        # make sure that dealing past the end of the deck returns None
        card = deck.deal_card()
        assert (
            card is None
        ), f"Expected dealing more than 52 cards to return None but returned {card}"
    except Exception as e:
        raise StudentError(
            f"Expected dealing more than 52 cards to return None."
            f"\nTrying to deal 53 cards produced the following error:"
            f"\n\t{type(e).__name__}:{e}"
        )

    # make sure the counter stayed at 52
    assert (
        str(deck).lower() == "deck(dealt 52, shuffled=false)"
    ), f"Dealing more than 52 cards incremented the counter past 52; got {str(deck).lower()}"

    # make sure the deck has all four suits
    deck.reset()
    suits = set()
    for _ in range(52):
        suits.add(deck.deal_card().suit.upper())
    assert all(
        suit in suits for suit in ("C", "H", "S", "D")
    ), f"Expected all four suits (C, H, S, D) but found {suits}"

    # make sure that dealing cards updates the counter
    deck = StudentDeck()
    deal_amt = random.randint(1, 52)
    [deck.deal_card() for _ in range(deal_amt)]
    assert (
        str(deck).lower() == f"deck(dealt {deal_amt}, shuffled=false)"
    ), f'`deal_card()` did not update the __repr__ correctly; expected "deck(dealt {deal_amt}, shuffled=False)" got {str(deck).lower()}'

    # make sure reset resets the counter
    deck.reset()
    assert (
        str(deck).lower() == f"deck(dealt 0, shuffled=false)"
    ), f'`reset()` did not reset the counter correctly; expected "deck(dealt 0, shuffled=false)" from the __repr__; got {str(deck).lower()}'

    # make sure the ranks of all 52 cards in the right order
    deck.reset()
    expected_ranks = list(range(2, 15)) * 4
    actual_ranks = [deck.deal_card().rank for _ in range(52)]
    assert (
        actual_ranks == expected_ranks
    ), f"Expected unshuffled deck to be in the order:\n\t{expected_ranks}\nbut got:\n\t{actual_ranks}"

    # make sure the suits of all 52 cards are correct
    deck.reset()
    suits = [deck.deal_card().suit for _ in range(52)]
    same_as_next = [suits[i] == suits[i + 1] for i in range(51)]
    expected = [True] * 12 + [False]
    expected *= 4
    expected = expected[:-1]
    assert (
        same_as_next == expected
    ), f"Expected the suits of an unshuffled deck to grouped together but they were not. Suits are:\n\t{suits}"
    assert (
        suits[-2] == suits[-2]
    ), f"Expected the suits of an unshuffled deck to grouped together but they were not. Suits are:\n\t{suits}"

    # make sure shuffling does something
    ordered_ranks = list(range(2, 15)) * 4
    ordered_suits = [True] * 12 + [False]
    ordered_suits *= 4
    ordered_suits = ordered_suits[:-1]
    deck_in_order = []
    for _ in range(10):
        deck.reset()
        deck.shuffle()
        actual_ranks = [deck.deal_card().rank for _ in range(52)]
        actual_suits = [suits[i] == suits[i + 1] for i in range(51)]
        deck_in_order.append(
            actual_ranks == ordered_ranks and actual_suits == ordered_suits
        )

    assert not all(
        deck_in_order
    ), "Shuffling is expected to change the order of cards but did not"

    # make sure resetting puts the cards back in order
    deck.reset()
    actual_ranks = [deck.deal_card().rank for _ in range(52)]
    actual_suits = [suits[i] == suits[i + 1] for i in range(51)]
    assert (
        actual_ranks == ordered_ranks and actual_suits == ordered_suits
    ), "`reset()` should put the deck back in order but it was still shuffled"

    print_passed(sha224(str.encode(__seed__[1])).hexdigest())
