# Spring 2018 Final exam skeletons and testing code.
# Lillian Lee and Anne Bracy


"""STUDENTS: Go to the script code at the end of the file and comment/uncomment
   to select which questions you want to work on and have your answers tested.

   Then, fill in the appropriate skeleton.

   Then, run python on this entire file.
"""


import inspect # To get names of calling function automatically
               # (lets you look at the actual call stack!)

# For capturing print() output to test it
# https://stackoverflow.com/questions/16571150/how-to-capture-stdout-output-from-a-python-function-call
import io
from contextlib import redirect_stdout




###### Q1
# Students: See solutions for link to Python Tutor version of the code, which
# you can copy/paste for yourself.

###### Q2(a)-(c)

def test_q2abc():
    # STUDENTS: Leave these lines alone
    print_test_fn_info("start")
    item1 = None
    item2 = None

    # STUDENTS: write in your answers to (a), (b) below.
    pass

    # STUDENTS: leave this code block alone, and put your answer to (c) below it.
    assert isinstance(item1, MenuItem), \
        assert_msg("The type of item1", "MenuItem", type(item1))
    assert not isinstance(item1, LunchItem), \
        "item1 is an instance of LunchItem but shouldn't be."
    assert item1.name == "Tofu Curry", \
        assert_msg("The name of item1", "Tofu Curry", repr(item1.name))
    assert item1.is_veggie, assert_msg("item1.is_veggie", True, False)
    assert item1.price == 24, assert_msg("item1.price", 24, item1.price)
    assert len(item1.__dict__) == 3, \
        "item1 has too many attributes: " + str(item1.__dict__)
    assert isinstance(item2, LunchItem), \
        assert_msg("The type of item2", "LunchItem", type(item2))
    # Short way to check the attributes of item2
    correct_attributes = {'name': 'Hamburger', 
                          'is_veggie': False, 
                          'price': 12, 
                          'lunch_price': 8}
    assert item2.__dict__ == correct_attributes, \
        "Something wrong with at least one of item2's attributes: " + str(item2.__dict__)

    # STUDENTS: write a line that breaks the invariant specifically on item2 here.
    pass


    # STUDENTS: leave this code block alone
    assert item2.lunch_price > 10, \
        assert_msg("item2.lunch_price", "more than 10", item2.lunch_price)



    print_test_fn_info("end")


###### Q2(d)

def audit_menu(the_menu):
    """Performs an audit of each LunchItem on the_menu, making sure that each 
    lunch_price is never more than 10 dollars. A lunch_price of 11 dollars is 
    changed to 9. An item whose lunch_price is more than 11 is too expensive to 
    be offered at lunch; it must be replaced with a new, equivalent MenuItem 
    (that has no lunch price). Items that are not LunchItems are unchanged.
    Modifies the_menu; does not create or return a new menu/list 
    the_menu: possibly empty list of MenuItem """
    pass # STUDENTS: to be implemented


# Students: leave this alone; it was implemented for you.
class MenuItem():
    """An instance represents an item on a menu."""
    def __init__(self, name, is_veggie, price):
        """A new menu item called name with 3 attributes:
        name:      a non-empty str, e.g. 'Chicken Noodle Soup'
        is_veggie: a Bool indicating vegetarian or not
        price:     an int > 0 """
        self.name = name
        self.is_veggie = is_veggie
        assert price > 0
        self.price = price

    # Added __eq__ method for testing 
    def __eq__(self, other):
        assert isinstance(other, MenuItem)
        for attrname in ['name', 'is_veggie', 'price']:
            # __dict__ is a dictionary of the attributes/values of an object!
            if self.__dict__[attrname] != other.__dict__[attrname]:
                return False
        return True
                

    # Added __repr__ method so that error printing of lists of M.I.s is clearer
    def __str__(self):
        nicetype = str(type(self)) # convert to nice version of this MenuItem's type
        nicetype = nicetype[nicetype.find('.')+1:-2]
        outstr = nicetype + " " + self.name + ". "
        outstr += "is_veggie: " + str(self.is_veggie)
        outstr += "; price: " + str(self.price)
        return outstr

    def __repr__(self):
        return self.__str__()


# Students: leave this alone; it was implemented for you.
class LunchItem(MenuItem):
    """An instance represents an item that can also be served at lunch"""
    def __init__(self, name, is_veggie, price, lunch_price):
        """A menu item with one additional attribute:
        lunch_price:  an  int > 0 and <= 10"""
        super().__init__(name, is_veggie, price)
        assert lunch_price > 0
        assert lunch_price <= 10
        self.lunch_price = lunch_price

    # Added __repr__ method so that error printing of lists of M.I.s is clearer
    def __repr__(self):
        outstr = super().__repr__()
        outstr += "; lunch price: " + str(self.lunch_price)
        return outstr

    def __str__(self):
        return self.__repr()


    # Added __eq__ method for testing 
    def __eq__(self, other):
        assert isinstance(other, LunchItem)
        return super().__eq__(other) and self.lunch_price == other.lunch_price

def test_q2d_audit_menu():
    print_test_fn_info("start")

    item0 = MenuItem("Fish Curry", False, 24)
    item1 = LunchItem("Hamburger", False, 12, 6)

    the_menu = [item0, item1]
    the_menu.append(MenuItem("Spaghetti", True, 14))
    the_menu.append(MenuItem("Spaghetti with Meatballs", False, 14))
    item4 = LunchItem("Tomato Soup", True, 10, 7)
    the_menu.append(item4)
    item4.lunch_price = 11 #  lunch_price is 11, should become 9
    item5 = LunchItem("Grilled Cheese", True, 10, 8)
    the_menu.append(item5)
    item5.lunch_price = 18 # lunch_price is too high, byebye from lunchhood

    audit_menu(the_menu)

    # Check whether the entries of the_menu are as expected
    correct = {0: MenuItem("Fish Curry", False, 24),
               1: LunchItem("Hamburger", False, 12, 6),
               2: MenuItem("Spaghetti", True, 14),
               3: MenuItem("Spaghetti with Meatballs", False, 14),
               4: LunchItem("Tomato Soup", True, 10, 9),
               5: MenuItem("Grilled Cheese", True, 10)}

    for i in range(len(the_menu)):
        right = correct[i]
        assert type(the_menu[i]) == type(right), \
            "the_menu["+str(i)+"] has wrong type " + str(type(the_menu[i]))
        assert the_menu[i] == right, \
            assert_msg("the_menu["+str(i)+"]", str(right), str(the_menu[i]))

    print_test_fn_info("end")

###### Q3
def after_at(s):
    """Returns a list of every non-empty sequence of non-space, non-@ characters
    that directly follows an @ in s.

    The elements should be ordered by occurrence in s, and there should be no repeats.

    Pre: s is a string, possible empty.
    """
    # STUDENTS: don't touch this code block.
    temp = s.split('@')
    if len(temp) == 1:  # There were no @s in s.
        return []
    afters_list = temp[1:]  # Drop stuff before 1st @. See table at bottom of page.

    # STUDENTS: provide the rest of the implementation
    # DON'T use split. (It sometimes calls strip(), which you don't want here.)
    # Hint: for each item in afters_list, use string method find() to find the
    #  location of the first space (if any)
    pass

def test_q3_after_at():
    print_test_fn_info("start")

    test_cases = {
        '@bill @ted meet @ the Circle K': ['bill', 'ted'],
        '@bill @ted meet @  the Circle K': ['bill', 'ted'],
        'meet @ the Circle K @Bill@Ted': ['Bill', 'Ted'],
        'hi': [],
        '@martha @Martha @martha': ['martha', 'Martha'],
        'The symbol du jour is an @': [],
         'The symbol du jour is an @!': ['!']
    }

    for s in test_cases:
        assert after_at(s) == test_cases[s], \
            assert_msg("after_at("+s+")", test_cases[s], after_at(s))
            
    print_test_fn_info("end")


####### Q4
def countdown_by_n(count_from, count_by):
    """Prints a count down from count_from by count_by. 
    Stops printing before the result goes negative.
    Note: this function does not return anything.

    count_from: the number you're counting down from [int]
    count_by: the amount you're counting down by [int > 0]

    Examples: 

    countdown_by_n(16, 5) should print:
       16
       11
       6
       1

    countdown_by_n(21, 7) should print:
       21
       14
       7
       0    

    """
    pass # STUDENTS: must make effective use of a while-loop.

def test_q4_countdown():
    print_test_fn_info("start")

    right= {
        # format; args to countdown_by_n, desired printout
        (16,5): "16\n11\n6\n1\n",
        (21,7): "21\n14\n7\n0\n"
    }

    for arg_pair in right:
        # https://stackoverflow.com/questions/16571150/how-to-capture-stdout-output-from-a-python-function-call
        f = io.StringIO()
        with redirect_stdout(f):
            countdown_by_n(*arg_pair) # breaks arg_pair into arguments
        out = f.getvalue()  # out is the string that would be printed out.

        right_answer = right[arg_pair]
        assert out==right[arg_pair], \
            assert_msg("countdown_by_n"+str(arg_pair)+" printout",
                        right_answer,
                        str(out))


    print_test_fn_info("end")

####### Q5
def requires(c, other_label):
    """Returns True if Course with label other_label must be taken before c,
    False otherwise.

    Pre: c is a Course.  other_label is a non-empty string."""
    # STUDENTS: FIND THE ERRORS in the code below.
    if len(c.prereqs) <= 1:
        return False
    else:
        for p in prereqs:
            if p.label == other_label:
                return True
            elif requires(other_label, c):
                return True
        return False

def test_q5_requires():
    print_test_fn_info("start")
    c1 = Course('CS1110', [])
    c2 = Course('CS2110', [c1])
    c3 = Course('CS2800', [c1])
    c4 = Course('CS3110', [c2, c3])

    # Cases that should be True
    for theinput in [(c4, 'CS2800'), (c4, 'CS2110'), (c4, 'CS1110')]:
        assert requires(*theinput), \
            assert_msg("requires"+repr(theinput), True, requires(*theinput))

    # Cases that should be False
    for theinput in [(c1, 'CS2800'), (c3, 'CS2800'), (c4, c2), (c4, 'randomstring')]:
        assert not requires(*theinput), \
           assert_msg("requires"+repr(theinput), False, requires(*theinput))



    print_test_fn_info("end")

# STUDENTS: leave this class definition alone
class Course():
    """An instance represents a course offered by a university.
    Courses are uniquely identified by their label.

    Instance variables:
        label [str] -- unique and non-empty, e.g., 'CS1110'
        prereqs [list of Course] -- courses that one must complete
        before one may enroll in this course. Possibly empty. """

    def __init__(self, label, prereqs):
        """A new course called label with prerequisites prereqs.
        Pre: label: a non-empty string (e.g., 'CS1110')
             prereqs: a (possibly empty) list of Course"""
        self.label = label
        self.prereqs = prereqs

    def __str__(self):
        return self.label



# helper
def assert_msg(subject, expected, result):
    return subject + " should be " + repr(expected) + ", but is " + repr(result)



# helper
def print_test_fn_info(kind):
    """If `kind` is the string "start", prints
        Running <name of enclosing function>
       If `kind` is the string "end", prints
        Hurrah, all test cases passed for <name of function>!\n"""

    enclosing_fn_name = inspect.stack()[1][3]

    if kind == "start":
        print("*** Running " + enclosing_fn_name)
    elif kind == "end":
        print("*** Hurrah, all test cases passed for  " + enclosing_fn_name + "!\n")



if __name__ == '__main__':

    # STUDENTS: comment out the names of test functions you don't want to run.
    # Or, uncomment the names of test functions you DO want to run. 
    tests_to_run = [
        test_q2abc,
        test_q2d_audit_menu,
        test_q3_after_at,
        test_q4_countdown,
        test_q5_requires
    ]

    for testfn in tests_to_run:
        testfn()  # the parentheses mean the function is called
