"""Tests for classes exercises"""
from datetime import timedelta
from unittest.mock import call, patch, sentinel
import unittest

from classes import (
    BankAccount,
    Flavor,
    Size,
    IceCream,
    Month,
    Row,
    MinimumBalanceAccount,
    Node,
    DoublyLinkedNode,
    IMAPChecker,
    AlphaString,
    MaxCounter,
    LastUpdatedDictionary,
    OrderedCounter,
)


class BankAccountTests(unittest.TestCase):

    """Tests for BankAccount."""

    def test_new_account_balance_default(self):
        account = BankAccount()
        self.assertEqual(account.balance, 0)

    def test_opening_balance(self):
        account = BankAccount(balance=100)
        self.assertEqual(account.balance, 100)

    def test_deposit(self):
        account = BankAccount()
        account.deposit(100)
        self.assertEqual(account.balance, 100)

    def test_withdraw(self):
        account = BankAccount(balance=100)
        account.withdraw(40)
        self.assertEqual(account.balance, 60)

    def test_repr(self):
        account = BankAccount()
        self.assertEqual(repr(account), "BankAccount(balance=0)")
        account.deposit(200)
        self.assertEqual(repr(account), "BankAccount(balance=200)")

    def test_transfer(self):
        mary_account = BankAccount(balance=100)
        dana_account = BankAccount(balance=0)
        mary_account.transfer(dana_account, 20)
        self.assertEqual(mary_account.balance, 80)
        self.assertEqual(dana_account.balance, 20)

    @unittest.skip("Comment this line for transactions exercise.")
    def test_transactions_open(self):
        expected_transactions = [
            ("OPEN", 100, 100),
        ]
        account = BankAccount(balance=100)
        self.assertEqual(account.transactions, expected_transactions)

    @unittest.skip("Comment this line for transactions exercise.")
    def test_transactions_deposit(self):
        expected_transactions = [
            ("OPEN", 0, 0),
            ("DEPOSIT", 100, 100),
        ]
        account = BankAccount()
        account.deposit(100)
        self.assertEqual(account.transactions, expected_transactions)

    @unittest.skip("Comment this line for transactions exercise.")
    def test_transactions_withdraw(self):
        expected_transactions = [
            ("OPEN", 100, 100),
            ("WITHDRAWAL", -40, 60),
        ]
        account = BankAccount(balance=100)
        account.withdraw(40)
        self.assertEqual(account.transactions, expected_transactions)

    @unittest.skip("Comment this line for transactions exercise.")
    def test_transactions_scenario(self):
        expected_transactions = [
            ("OPEN", 0, 0),
            ("DEPOSIT", 100, 100),
            ("WITHDRAWAL", -40, 60),
            ("DEPOSIT", 95, 155),
        ]
        account = BankAccount()
        account.deposit(100)
        account.withdraw(40)
        account.deposit(95)
        self.assertEqual(account.transactions, expected_transactions)

    @unittest.skip("Comment this line for truthy accounts exercise.")
    def test_truthy_accounts(self):
        account = BankAccount()
        self.assertIs(bool(account), False)
        account.deposit(100)
        self.assertIs(bool(account), True)

    @unittest.skip("Comment this line for BankAccount comparison exercise.")
    def test_account_comparisons(self):
        account1 = BankAccount()
        account2 = BankAccount()
        self.assertTrue(account1 == account2)
        self.assertTrue(account1 >= account2)
        self.assertTrue(account1 <= account2)
        account1.deposit(100)
        account2.deposit(10)
        self.assertTrue(account1 != account2)
        self.assertTrue(account2 < account1)
        self.assertTrue(account1 > account2)
        self.assertTrue(account2 < account1)
        self.assertTrue(account1 >= account2)
        self.assertTrue(account2 <= account1)


class FlavorTests(unittest.TestCase):

    """Tests for Flavor."""

    def test_name_attribute(self):
        flavor = Flavor("vanilla")
        self.assertEqual(flavor.name, "vanilla")

    def test_specifying_ingredients(self):
        flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"])
        self.assertEqual(flavor.ingredients, ["milk", "sugar", "vanilla"])
        flavor = Flavor(
            "chocolate",
            ingredients=["milk", "sugar", "vanilla", "chocolate"],
        )
        self.assertEqual(
            flavor.ingredients,
            ["milk", "sugar", "vanilla", "chocolate"],
        )

    def test_modifying_ingredients(self):
        original_ingredients = ["milk", "sugar", "vanilla"]
        flavor = Flavor("vanilla", ingredients=original_ingredients)
        flavor.ingredients.append("red bean")
        self.assertEqual(original_ingredients, ["milk", "sugar", "vanilla"])

    def test_has_dairy_attribute(self):
        flavor = Flavor("vanilla")
        self.assertIs(flavor.has_dairy, True)
        flavor = Flavor("vanilla", has_dairy=True)
        self.assertIs(flavor.has_dairy, True)
        flavor = Flavor("vanilla", has_dairy=False)
        self.assertIs(flavor.has_dairy, False)

    def test_string_representation(self):
        flavor = Flavor("chocolate", has_dairy=False)
        self.assertEqual(
            repr(flavor),
            "Flavor(name='chocolate', ingredients=[], has_dairy=False)",
        )
        flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"])
        self.assertEqual(
            repr(flavor),
            "Flavor(name='vanilla', ingredients=['milk', 'sugar', 'vanilla'], has_dairy=True)",
        )


class SizeTests(unittest.TestCase):

    """Tests for Size."""

    def test_initializer(self):
        size = Size(quantity=1, unit="gram", price="5.00")
        self.assertEqual(size.quantity, 1)
        self.assertEqual(size.unit, "gram")
        self.assertEqual(size.price, "5.00")

    def test_human_string_representation(self):
        size = Size(quantity=1, unit="gram", price="5.00")
        self.assertEqual(str(size), "1 gram")
        size = Size(quantity=1, unit="scoop", price="5.00")
        self.assertEqual(str(size), "1 scoop")

    def test_pluralization(self):
        size = Size(quantity=3, unit="pint", price="9.00")
        self.assertEqual(str(size), "3 pints")
        size = Size(quantity=3, unit="scoop", price="4.00")
        self.assertEqual(str(size), "3 scoops")

    def test_machine_string_representation(self):
        size = Size(quantity=1, unit="cup", price="5")
        self.assertEqual(repr(size), "Size(quantity=1, unit='cup', price='5')")


class IceCreamTests(unittest.TestCase):

    """Tests for IceCream."""

    def test_initializer(self):
        one_quart = Size(quantity=1, unit="quart", price="$9")
        vanilla = Flavor("vanilla")
        quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart)
        self.assertEqual(quart_of_vanilla.size, one_quart)
        self.assertEqual(quart_of_vanilla.flavor, vanilla)

    def test_string_representation(self):
        one_quart = Size(quantity=1, unit="quart", price="$9")
        vanilla = Flavor("vanilla")
        quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart)
        self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla")
        self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla")
        two_scoops = IceCream(
            flavor=Flavor("chocolate"),
            size=Size(quantity=2, unit="scoop", price="$3"),
        )
        self.assertEqual(str(two_scoops), "2 scoops of chocolate")


class MonthTests(unittest.TestCase):

    """Tests for Month."""

    def test_initialization(self):
        month = Month(2019, 1)
        self.assertEqual(month.year, 2019)
        self.assertEqual(month.month, 1)

    def test_machine_readable_representation(self):
        month = Month(2019, 1)
        self.assertEqual(repr(month), "Month(2019, 1)")

    def test_human_readable_representation(self):
        month = Month(2019, 1)
        self.assertEqual(str(month), "2019-01")

    def test_first_method(self):
        month = Month(2019, 1)
        date = month.first()
        self.assertEqual(date.year, 2019)
        self.assertEqual(date.month, 1)
        self.assertEqual(date.day, 1)
        self.assertEqual(str(date), "2019-01-01")
        self.assertEqual(str(date - timedelta(days=1)), "2018-12-31")


class RowTests(unittest.TestCase):

    """Tests for Row."""

    def test_no_arguments(self):
        row = Row()
        attributes = {x for x in dir(row) if not x.startswith("__")}
        self.assertEqual(attributes, set())

    def test_single_argument(self):
        row = Row(a=1)
        self.assertEqual(row.a, 1)
        attributes = {x for x in dir(row) if not x.startswith("__")}
        self.assertEqual(attributes, {"a"})

    def test_two_arguments(self):
        row = Row(a=1, b=2)
        self.assertEqual(row.a, 1)
        self.assertEqual(row.b, 2)
        attributes = {x for x in dir(row) if not x.startswith("__")}
        self.assertEqual(attributes, {"a", "b"})

    def test_many_arguments(self):
        row = Row(thing="a", item=2, stuff=True)
        self.assertEqual(row.thing, "a")
        self.assertEqual(row.item, 2)
        self.assertEqual(row.stuff, True)
        attributes = {x for x in dir(row) if not x.startswith("__")}
        self.assertEqual(attributes, {"thing", "item", "stuff"})

    def test_no_positional_arguments_accepted(self):
        with self.assertRaises(Exception):
            Row(1, 2)
        with self.assertRaises(Exception):
            Row(1)


class MinimumBalanceAccountTests(unittest.TestCase):

    """Tests for MinimumBalanceAccount."""

    def test_withdraw_from_new_account(self):
        account = MinimumBalanceAccount()
        with self.assertRaises(ValueError):
            account.withdraw(1)

    def test_exception_message(self):
        account = MinimumBalanceAccount()
        with self.assertRaises(ValueError) as cm:
            account.withdraw(1000)
        self.assertEqual(str(cm.exception), "Balance cannot be less than $0")

    def test_withdraw_above_zero(self):
        account = MinimumBalanceAccount()
        account.deposit(100)
        account.withdraw(99)
        self.assertEqual(account.balance, 1)

    def test_withdraw_to_exactly_zero(self):
        account = MinimumBalanceAccount()
        account.deposit(100)
        account.withdraw(100)
        self.assertEqual(account.balance, 0)

    def test_withdraw_to_below_zero(self):
        account = MinimumBalanceAccount()
        account.deposit(100)
        with self.assertRaises(ValueError):
            account.withdraw(101)

    def test_repr(self):
        account = MinimumBalanceAccount()
        self.assertEqual(repr(account), "MinimumBalanceAccount(balance=0)")


class IMAPCheckerTests(unittest.TestCase):

    """Tests for IMAPChecker."""

    def test_initialization(self):
        host = "example.com"
        with patch("classes.IMAP4_SSL", autospec=True) as imap_mock:
            IMAPChecker(host)
        self.assertEqual(imap_mock.mock_calls, [call(host)])

    def test_authentication(self):
        host = "example.com"
        username = "user@example.com"
        password = "password"
        with patch("classes.IMAP4_SSL", autospec=True) as imap_mock:
            checker = IMAPChecker(host)
            checker.authenticate(username, password)
        self.assertEqual(imap_mock.mock_calls, [
            call(host),
            call().login(username, password),
            call().select('inbox'),
        ])

    def test_get_message_uids(self):
        host = "example.com"
        with patch("classes.IMAP4_SSL", autospec=True) as imap_mock:
            checker = IMAPChecker(host)
            uids = checker.get_message_uids()
        imap_mock.assert_has_calls([call().uid("search", None, "ALL")])
        self.assertEqual(
            uids,
            (imap_mock.return_value.uid.return_value.__getitem__.return_value
             .__getitem__.return_value.split.return_value),
        )

    def test_get_message(self):
        host = "example.com"
        uid = "uid1"
        with patch("classes.IMAP4_SSL", autospec=True) as imap_mock:
            with patch("classes.Parser", autospec=True) as parser_mock:
                imap_mock.return_value.uid.return_value = [
                    "",
                    (("", sentinel.MessageText), ""),
                ]
                parser_mock.return_value.parsestr.return_value = sentinel.M
                checker = IMAPChecker(host)
                message = checker.get_message(uid)
        self.assertEqual(imap_mock.mock_calls, [
            call(host),
            call().uid('fetch', uid, '(RFC822)')
        ])
        self.assertEqual(parser_mock.mock_calls, [
            call(),
            call().parsestr(sentinel.MessageText),
        ])
        self.assertEqual(message, sentinel.M)


class NodeTests(unittest.TestCase):

    """Tests for Node."""

    def test_single_node(self):
        self.assertEqual(str(Node("A")), "A")

    def test_multiple_nodes(self):
        expected = (
            "Animalia / Chordata / Mammalia / Carnivora / Ailuridae "
            "/ Ailurus / A. fulgens"
        )
        red_panda = (
            Node("Animalia")
            .make_child("Chordata")
            .make_child("Mammalia")
            .make_child("Carnivora")
            .make_child("Ailuridae")
            .make_child("Ailurus")
            .make_child("A. fulgens")
        )
        self.assertEqual(str(red_panda), expected)


class DoublyLinkedNodeTests(unittest.TestCase):

    """Tests for DoublyLinkedNode."""

    def test_single_node(self):
        t = DoublyLinkedNode("A")
        leaves = [node.name for node in t.leaves()]
        self.assertEqual(leaves, ["A"])
        self.assertIs(t.is_leaf(), True)

    def test_multiple_nodes(self):
        root = DoublyLinkedNode("A")
        child1 = root.make_child("1")
        grandchild1 = child1.make_child("a")
        grandchild2 = child1.make_child("b")
        child2 = root.make_child("2")
        leaves0 = [node.name for node in root.leaves()]
        leaves1 = [node.name for node in child1.leaves()]
        leaves2 = [node.name for node in child2.leaves()]
        self.assertEqual(leaves0, ["a", "b", "2"])
        self.assertEqual(leaves1, ["a", "b"])
        self.assertEqual(leaves2, ["2"])
        self.assertIs(grandchild1.is_leaf(), True)
        self.assertIs(grandchild2.is_leaf(), True)
        self.assertIs(child1.is_leaf(), False)
        self.assertIs(child2.is_leaf(), True)


class AlphaStringTests(unittest.TestCase):

    """Tests for AlphaString."""

    def test_good_alpha(self):
        s = AlphaString("abcdefg")
        self.assertEqual(s, "abcdefg")

    def test_bad_string(self):
        with self.assertRaises(ValueError):
            AlphaString("abc123defg")


class MaxCounterTests(unittest.TestCase):

    """Tests for MaxCounter."""

    def test_works_like_counter(self):
        counts = MaxCounter("hello")
        self.assertEqual(counts, {"h": 1, "e": 1, "l": 2, "o": 1})
        self.assertEqual(counts["h"], 1)
        self.assertEqual(counts["!"], 0)

    def test_single_maximum(self):
        counts = MaxCounter("hello")
        self.assertEqual(set(counts.max_keys()), {"l"})

    def test_multiple_maximums(self):
        counts = MaxCounter("no banana")
        self.assertEqual(set(counts.max_keys()), {"a", "n"})

    def test_all_maximums(self):
        counts = MaxCounter("abcd")
        self.assertEqual(set(counts.max_keys()), set("abcd"))

    def test_empty(self):
        counts = MaxCounter("")
        self.assertEqual(set(counts.max_keys()), set())


class LastUpdatedDictionaryTests(unittest.TestCase):

    """Tests for LastUpdatedDictionary."""

    def test_initial_order(self):
        d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)])
        self.assertEqual(list(d.keys()), ["a", "c", "b", "d"])
        self.assertEqual(list(d.values()), [1, 3, 2, 4])

    def test_order_after_insertion(self):
        d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)])
        d["e"] = 5
        self.assertEqual(list(d.keys()), ["a", "c", "b", "d", "e"])
        self.assertEqual(list(d.values()), [1, 3, 2, 4, 5])

    def test_order_after_update(self):
        d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)])
        d["c"] = 0
        self.assertEqual(list(d.keys()), ["a", "b", "d", "c"])
        self.assertEqual(list(d.values()), [1, 2, 4, 0])


class OrderedCounterTests(unittest.TestCase):

    """Tests for OrderedCounter."""

    def test_initial_order(self):
        c = OrderedCounter("hello world")
        self.assertEqual(
            list(c.keys()),
            ["h", "e", " ", "w", "o", "r", "l", "d"],
        )
        self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1])

    def test_order_after_insertion(self):
        c = OrderedCounter("hello world")
        c.update("cat")
        self.assertEqual(
            list(c.keys()),
            ["h", "e", " ", "w", "o", "r", "l", "d", "c", "a", "t"],
        )
        self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1])

    def test_order_after_update(self):
        c = OrderedCounter("hello world")
        c.update("hey")
        self.assertEqual(
            list(c.keys()),
            [" ", "w", "o", "r", "l", "d", "h", "e", "y"],
        )
        self.assertEqual(list(c.values()), [1, 1, 2, 1, 3, 1, 2, 2, 1])


if __name__ == "__main__":
    from helpers import error_message

    error_message()
