class TestVendingMachine(unittest.TestCase):
    def setUp(self):
        self.vendmachine = VendingMachine()
        self.quarter = Coin(weight=5.670, diameter=24.26, thickness=1.75)
        self.dime = Coin(weight=2.268, diameter=17.91, thickness=1.35)
        self.nickle = Coin(weight=5.000, diameter=21.21, thickness=1.95)
        self.penny = Coin(weight=2.500, diameter=19.05, thickness=1.52)
        # stock the machine
        for item in ["cola", "candy", "chips"]:
            map(self.vendmachine.inventory[item].append, repeat(item, 20))

    def test_product_price_is_int(self):
        self.assertTrue(isinstance(self.vendmachine.get_product_price("chips"), int))

    def test_product_to_dollar_string_price(self):
        self.assertEqual(self.vendmachine.product_to_dollar_string_price("candy"), "$0.65")
        self.assertEqual(self.vendmachine.product_to_dollar_string_price("cola"), "$1.00")
        self.assertEqual(self.vendmachine.product_to_dollar_string_price("chips"), "$0.50")

    def test_product_list_exists(self):
        self.assertTrue(isinstance(self.vendmachine.get_product_list(), list))

    def test_detect_coin_value(self):
        self.assertEqual(self.vendmachine.detect_coin_value(self.quarter), 25)
        self.assertEqual(self.vendmachine.detect_coin_value(self.dime), 10)
        self.assertEqual(self.vendmachine.detect_coin_value(self.nickle), 5)
        self.assertEqual(self.vendmachine.detect_coin_value(self.penny), None)

    def test_current_balance_in_machine_is_zero_on_startup(self):
        newmachine = VendingMachine()
        self.assertEqual(newmachine.get_current_balance(), 0)

    def test_inserted_coin_updates_current_balance(self):
        current_balance = self.vendmachine.get_current_balance()
        current_coin_return = self.vendmachine.check_coin_return()
        # insert the nickle and check that it is not returned
        self.vendmachine.insert_coin(self.nickle)
        # check that the coin return has not changed
        self.assertEqual(self.vendmachine.check_coin_return(), current_coin_return)
        self.assertEqual(self.vendmachine.get_current_balance(), current_balance + 5)

    def test_inserted_penny_is_returned(self):
        vendmachine = VendingMachine()
        current_balance = vendmachine.get_current_balance()
        # penny should be returned when inserted
        vendmachine.insert_coin(self.penny)
        # check that the penny was placed in the coin return
        self.assertEqual(vendmachine.check_coin_return(), [self.penny])
        # current balance should not have changed
        self.assertEqual(vendmachine.get_current_balance(), current_balance)

    def test_empty_out_coins_from_machine(self):
        coin_collection = self.vendmachine.take_out_coins()
        self.assertTrue(isinstance(coin_collection, list))
        updated_coin_collection = self.vendmachine.take_out_coins()
        self.assertEqual(updated_coin_collection, [])

    def test_inserted_coins_are_added_to_bank_after_purchase(self):
        _ = self.vendmachine.take_out_coins()
        # insert $0.50
        my_coins = [self.quarter, self.quarter]
        map(self.vendmachine.insert_coin, my_coins)
        # no coins should be in bank before purchase
        self.assertEqual(self.vendmachine.take_out_coins(), [])
        # purchase chips
        self.vendmachine.dispense_product("chips")
        # coins should now be in the coin bank
        self.assertEqual(self.vendmachine.take_out_coins(), my_coins)

    def test_display_shows_insert_coins(self):
        vendmachine = VendingMachine()
        # add coins to vending machine so that it does not display the exact change only message
        map(vendmachine.add_to_coin_bank, [self.dime, self.nickle])
        current_balance = vendmachine.get_current_balance()
        if current_balance < vendmachine.get_product_price("cola"):
            self.assertEqual(vendmachine.check_display(), "INSERT COINS")

    def test_dispense_product(self):
        # add coins to vending machine so that it does not display the exact change only message
        map(self.vendmachine.add_to_coin_bank, [self.dime, self.nickle])
        for i in range(2):
            my_product = self.vendmachine.dispense_product("chips")
            self.assertEqual(my_product, None)  # no coins inserted
            self.assertEqual(self.vendmachine.check_display(), "PRICE $0.50")
            self.assertEqual(self.vendmachine.check_display(), "INSERT COINS")
        # insert quarters
        value_inserted = 0
        for i in range(2):
            self.vendmachine.insert_coin(self.quarter)
            value_inserted += 25
            self.assertEqual(self.vendmachine.check_display(), "$0.{0}".format(value_inserted))
        my_product = self.vendmachine.dispense_product("chips")
        self.assertEqual(my_product, "chips")
        self.assertEqual(self.vendmachine.check_display(), "THANK YOU")
        self.assertEqual(self.vendmachine.current_balance, 0)
        self.assertEqual(self.vendmachine.check_display(), "INSERT COINS")

    def test_return_coins(self):
        vendmachine = VendingMachine()
        # check that the coin return is empty
        self.assertEqual(vendmachine.check_coin_return(), [])
        # insert coins
        my_coins = [self.quarter, self.quarter, self.dime]
        for coin in my_coins:
            vendmachine.insert_coin(coin)
        vendmachine.return_coins()
        change = vendmachine.take_out_coins_from_coin_return()
        self.assertEqual(self.count_change(my_coins), self.count_change(change))
        # check that the coin return is empty again
        self.assertEqual(vendmachine.check_coin_return(), [])

    def count_change(self, change_list):
        change_value = 0
        for coin in change_list:
            change_value += self.vendmachine.detect_coin_value(coin)
        return change_value

    def count_returned_change(self):
        # count returned change
        return self.count_change(self.vendmachine.take_out_coins_from_coin_return())

    def test_make_change(self):
        # make sure coin return is empty
        if not len(self.vendmachine.check_coin_return()) == 0:
            # take out all the coins
            _ = self.vendmachine.take_out_coins_from_coin_return()
        # insert $0.90
        map(self.vendmachine.insert_coin, [self.quarter, self.quarter, self.quarter, self.dime, self.nickle])
        # buy candy
        self.vendmachine.dispense_product("candy")
        # check coin return
        self.assertEqual(25, self.count_returned_change())
        # insert $0.75
        map(self.vendmachine.insert_coin, repeat(self.quarter, 3))
        # buy candy again
        self.vendmachine.dispense_product("candy")
        self.assertEqual(10, self.count_returned_change())

    def test_exact_change_only(self):
        # take out all the coins from the machine
        _ = self.vendmachine.take_out_coins()
        # test that the screen displays that the exact change is needed on each check of the display
        for i in range(5):
            self.assertEqual("EXACT CHANGE ONLY", self.vendmachine.check_display())

    def test_sold_out(self):
        # buy all the cola in the machine
        while len(self.vendmachine.inventory["cola"]) > 0:
            # insert $1.00
            map(self.vendmachine.insert_coin, repeat(self.quarter, 4))
            # buy cola
            self.assertEqual("cola", self.vendmachine.dispense_product("cola"))
        # insert $1.00
        map(self.vendmachine.insert_coin, repeat(self.quarter, 4))
        # try to buy cola
        self.assertEqual(None, self.vendmachine.dispense_product("cola"))
        # check that it displays that the item is sold out
        self.assertEqual("SOLD OUT", self.vendmachine.check_display())