"""
This module lets you practice two forms of the ACCUMULATOR pattern:
  -- SUMMING
  -- COUNTING
where the accumulation is done via ITERATING (i.e., looping)
through a SEQUENCE.

It also demonstrates using an ORACLE and/or PROBABILITY THEORY
in testing and BOUNDARY (EDGE) TESTING.

Authors: David Mutchler, Vibha Alangar, Dave Fisher, Matt Boutell, Mark Hays,
         Mohammed Noureddine, Sana Ebrahimi, Sriram Mohan, their colleagues and
         PUT_YOUR_NAME_HERE.
"""  # TODO: 1. PUT YOUR NAME IN THE ABOVE LINE.

import random
import builtins  # Never necessary, but here to make a point about SUM
import math
import time

# -----------------------------------------------------------------------------
# TODO: 2. Watch the VIDEO for this module listed in the Follow-Me section
#  of the Preparation for this session.  It explains the various kinds of
#  TESTING that are used in this module.
#    After you have watched that video, mark this _TODO_ as DONE.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# The first of the two statements below makes each run of the program
#   use the same sequence of pseudo-random numbers.
# The second of the two statements below makes each run of the program
#   use a hard-to-predict sequence of pseudo-random numbers (one that depends
#   on the time of day), hence makes the sequence differ from run to run
#   in a way that appears random.
# -----------------------------------------------------------------------------

# random.seed(42)
random.seed((time.time() * 100) % 1000)


def main():
    """ Calls the   TEST   functions in this module. """
    run_test_sum_sequence()
    run_test_count_items_bigger_than()
    run_test_count_positive_sines()
    run_test_sum_first_n()


def run_test_sum_sequence():
    """ Tests the   sum_sequence   function. """
    print()
    print("--------------------------------------------------")
    print("Testing the   sum_sequence   function:")
    print("--------------------------------------------------")

    # -------------------------------------------------------------------------
    # TODO: 3. READ the COMMENTS and CODE in this function,
    #   asking questions as needed.
    #   ___
    #   When you believe that you understand:
    #     -- What an ORACLE is
    #     -- How one can generate and use RANDOM test cases
    #     -- How one can test using PROBABILITY THEORY
    #   then:
    #      change the above _TODO_ to DONE.
    # -------------------------------------------------------------------------

    # -------------------------------------------------------------------------
    # Here (below) are examples of using an ORACLE for testing,
    # that is, using a separate way of gaining the correct tests as if
    # by "magic". The oracle here is the built-in    sum    function.
    # We provided two tests that use that oracle.
    #
    # BTW, google for  "Oracle of Delphi" if you are curious about
    # why we call such tests "oracles".
    # -------------------------------------------------------------------------

    # -------------------------------------------------------------------------
    # Test 1 (using an ORACLE to computer the expected answer):
    # -------------------------------------------------------------------------
    sequence1 = [48, -10, 100, 9939309808, 433443080, -45634930]

    oracle_answer = builtins.sum(sequence1)
    actual_answer = sum_sequence(sequence1)

    print()
    print("Test 1: Using the sequence:")
    print("   ", sequence1)
    print("  Expected (oracle) result: ", oracle_answer)
    print("  Actual result:            ", actual_answer)

    # -------------------------------------------------------------------------
    # Test 2 (using an ORACLE to computer the expected answer):
    # -------------------------------------------------------------------------
    sequence2 = [48, 180, -475, 205, 88]

    oracle_answer = builtins.sum(sequence2)
    actual_answer = sum_sequence(sequence2)

    print()
    print("Test 2: Using the sequence:")
    print("   ", sequence2)
    print("  Expected (oracle) result: ", oracle_answer)
    print("  Actual result:            ", actual_answer)

    # -------------------------------------------------------------------------
    # Test 3 (using an ORACLE to compute the expected answer):
    #
    #   This test uses a RANDOMLY generated sequence,
    #   so every time you run the program it does a DIFFERENT test!
    #   So this code snippet can be used to do MANY tests!
    # -------------------------------------------------------------------------

    # The next few lines make a sequence of 10,000 RANDOM numbers:
    sequence3 = []
    for _ in range(10000):
        sequence3.append(random.randrange(-10, 11))

    oracle_answer = builtins.sum(sequence3)
    actual_answer = sum_sequence(sequence3)

    print()
    print("Test 3: Using the following RANDOMLY generated sequence:")
    print("  Un-comment the relevant line of code if you want")
    print("  to see the actual sequence.")
    # print("   ", sequence3)
    print("  Expected (oracle) result: ", oracle_answer)
    print("  Actual result:            ", actual_answer)

    # -------------------------------------------------------------------------
    # Tests 4 and 5:  using a KNOWN answer
    #   (here, ones easily computed by hand).]
    #
    #   Test 5 is an example of BOUNDARY (aka EDGE) testing, which is:
    #
    #       Where test cases are generated using the EXTREMES of the
    #       input domain, e.g. maximum, minimum, just inside/outside
    #       boundaries, error values. It focuses] on "corner cases".
    #
    #   The above quotation is a slight paraphrase from the Wikipedia
    #   article at https://en.wikipedia.org/wiki/Boundary_testing.
    # -------------------------------------------------------------------------

    # Test 4:
    sequence4 = [48, -10]

    known_answer = 38
    actual_answer = sum_sequence(sequence4)

    print()
    print("Test 4: Using the sequence:")
    print("   ", sequence4)
    print("  Expected (known) result: ", known_answer)
    print("  Actual result:           ", actual_answer)

    # Test 5:
    sequence5 = []

    known_answer = 0
    actual_answer = sum_sequence(sequence5)

    print()
    print("Test 5: Using the sequence:")
    print("   ", sequence5)
    print("  Expected (known) result: ", known_answer)
    print("  Actual result:           ", actual_answer)

    # -------------------------------------------------------------------------
    # Test 6:  (Don't worry if you don't follow this example fully.)
    #
    #   Like Test 3, this test uses a RANDOMLY generated sequence.
    #
    #   But unlike Test 3 (which used an ORACLE),
    #   THIS example uses PROBABILITY THEORY to predict (approximately)
    #   the expected value.
    #
    #   It relies on what is called the
    #      Law of Large Numbers
    #   which, as applied here says:
    #      If you compute the average of a lot of numbers with each
    #      number drawn RANDOMLY from -10 to 10 (inclusive),
    #      the result should be close to the average of the numbers
    #      from -10 to 10 (inclusive) [which is 0].
    #
    #   See https://en.wikipedia.org/wiki/Law_of_large_numbers
    #   for a not-too-clear explanation of the Law of Large Numbers.
    # -------------------------------------------------------------------------

    # Skips this test if  sum_sequence  has not yet been implemented:
    if sum_sequence([1, 2, 3]) == None:
        return

    sequence6 = []  # Next lines make a sequence of 10000 RANDOM numbers
    for _ in range(10000):
        sequence6.append(random.randrange(-10, 11))

    expected_sum_from_probability_theory = 0
    expected_average_from_probability_theory = 0
    actual_sum = sum_sequence(sequence6)
    actual_average = sum_sequence(sequence6) / 10000

    print()
    print("Test 6: Using the following RANDOMLY generated sequence:")
    print("  Un-comment the relevant line of code to see the actual sequence.")
    # print("   ", sequence6)

    print("  Expected results (from PROBABILITY THEORY):")
    print("    Sum:     ", expected_sum_from_probability_theory)
    print("    Average: ", expected_average_from_probability_theory)
    print("  ACTUAL results (should be CLOSE to the above)")
    print("    Sum:     ", actual_sum)
    print("    Average: ", actual_average)
    print("  where 'close' for the sum means absolute value < about 1216")
    print("  most of the time (about 19 times out of 20)")


def sum_sequence(sequence):
    """
    What comes in:  A sequence of integers.
    What goes out: Returns the sum of the numbers in the given sequence.
    Side effects: None.
    Examples:
      sum_sequence([8, 13, 7, -5])  returns  23
      sum_sequence([48, -10])       returns  38
      sum_sequence([])              returns  0
    Type hints:
      :type sequence: list or tuple (of integers)
    """
    # -------------------------------------------------------------------------
    # TODO: 4. Implement and test this function.
    #          Tests have been written for you (above).
    #   ___
    #   RESTRICTION:
    #     You may NOT use the built-in  sum   function
    #     in IMPLEMENTING this function.
    #       -- The TESTING code above does use   built_ins.sum
    #          as an ORACLE in TESTING this function, however.
    # -------------------------------------------------------------------------


def run_test_count_items_bigger_than():
    """ Tests the   count_items_bigger_than   function. """
    # -------------------------------------------------------------------------
    # TODO: 5. Implement this TEST function.
    #   It TESTS the  count_items_bigger_than  function defined below.
    #   Include at least ** 2 ** ADDITIONAL tests.
    #
    #   As usual, include both EXPECTED and ACTUAL results in your test
    #   and compute the latter BY HAND (not by running your program).
    # -------------------------------------------------------------------------
    print()
    print("--------------------------------------------------")
    print("Testing the   count_items_bigger_than   function:")
    print("--------------------------------------------------")

    # Test 1:
    sequence = [45, 84, 32, 70, -10, 40]
    threshold = 45
    expected = 2
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 1 expected:", expected)
    print("       actual:  ", actual)

    # Test 2:
    sequence = [45, 84, 32, 70, -10, 40]
    threshold = 0
    expected = 5
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 2 expected:", expected)
    print("       actual:  ", actual)

    # Test 3:
    sequence = [45, 84, 32, 70, -10, 40]
    threshold = -10
    expected = 5
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 3 expected:", expected)
    print("       actual:  ", actual)

    # Test 4:
    sequence = [45, 84, 32, 70, -10, 40]
    threshold = -10.000000001
    expected = 6
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 4 expected:", expected)
    print("       actual:  ", actual)

    # Test 5:
    sequence = []
    threshold = -99999999999999999999999999999
    expected = 0
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 5 expected:", expected)
    print("       actual:  ", actual)

    # Test 6:  This test uses a RANDOMLY generated sequence
    #          but a KNOWN answer (NONE in the sequence are > threshold)

    # Next lines make a sequence of 100,000 RANDOM numbers,
    # with each chosen from -100 to 99 (inclusive):
    sequence = []
    for _ in range(100000):
        sequence.append(random.randrange(-100, 100))

    threshold = 99.000000001
    expected = 0
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 6 expected:", expected)
    print("       actual:  ", actual)

    # Test 7:  This test uses a RANDOMLY generated sequence
    #          but a KNOWN answer (ALL in the sequence are > threshold).

    # Next lines make a sequence of 100,000 RANDOM numbers,
    # with each chosen from -100 to 99 (inclusive):
    sequence = []
    for _ in range(100000):
        sequence.append(random.randrange(-100, 100))

    threshold = -100.00000001
    expected = 100000
    actual = count_items_bigger_than(sequence, threshold)
    print()
    print("Test 7 expected:", expected)
    print("       actual:  ", actual)

    # Test 8:  This test uses a RANDOMLY generated sequence
    #          and an APPROXIMATE answer that is PROBABLY about right,
    #          based on PROBABILITY THEORY (the Law of Large Numbers).

    # Next lines make a sequence of 100,000 RANDOM numbers,
    # with each chosen from -100 to 99 (inclusive):
    sequence = []
    n = 100000
    for _ in range(n):
        sequence.append(random.randrange(-100, 100))

    threshold = 98
    expected = n * (1 / 200)
    actual = count_items_bigger_than(sequence, threshold)
    standard_deviation = math.sqrt(n * (1 / 200) * (199 / 200))

    print()
    print("Test 8 uses PROBABILITY THEORY")
    print("  to compute the expected result.")
    print("  See the note that follows to see")
    print("  how to evaluate the results of this test.")
    print("       expected:", expected)
    print("       actual:  ", actual)

    print()
    print("  Note on how to evaluate the results of")
    print("  Test 8 (above): According to Probability Theory,")
    message = "  the above 'actual' should be within {:.0f}"
    print(message.format(2 * standard_deviation))
    print("  of the above 'expected' about 95 percent of the time.")
    print()
    print("  You might try running this program multiple times")
    print("  to see whether or not that seems to be true")
    print("  for your code (and Python's pseudo-random numbers).")

    # TODO: 5 (continued):  Add your 2 ADDITIONAL tests here:


def count_items_bigger_than(numbers, threshold):
    """
    What comes in:
      -- An sequence of numbers.
      -- A number that is a "threshold".
    What goes out:
      Returns the number of items in the given sequence of numbers
      that are strictly bigger than the given "threshold" number.
    Side effects: None.
    Examples:
      If  numbers   is  [45, 84, 32, 70, -10, 40]
      and threshold is 45,
      then this function returns 2 (since 84 and 70 are bigger than 45).

      If  numbers   is  [45, 84, 32, 70, -10, 40]
      and threshold is 0,
      then this function returns 5
      (since all of the 6 numbers except -10 are bigger than 0).

      If  numbers   is  [45, 84, 32, 70, -10, 40]
      and threshold is -10,
      then this function still returns 5
      (since all of the 6 numbers except -10 are bigger than -10;
      note that -10 is NOT bigger than itself).

      If  numbers   is  [45, 84, 32, 70, -10, 40]
      and threshold is -10.00000001,
      then this function returns 6.

    Type hints:
      :type numbers:   list or tuple (of numbers)
      :type threshold: float
    """
    # -------------------------------------------------------------------------
    # TODO: 6. Implement and test this function.
    #   Note that you should write its TEST function first (above).
    # -------------------------------------------------------------------------


def run_test_count_positive_sines():
    """ Tests the   count_positive_sines   function. """
    # -------------------------------------------------------------------------
    # TODO: 7. Implement this TEST function.
    #   It TESTS the  count_positive_sines  function defined below.
    #   Include at least ** 1 ** ADDITIONAL test beyond what we supplied.
    #
    #   As usual, include both EXPECTED and ACTUAL results in your test
    #   and compute the latter BY HAND (not by running your program).
    #
    # NOTE: Some numbers that you might expect to be zero,
    #   for example math.sin(math.pi), are in fact slightly positive.
    #   That is because   math.pi   is not exact (nor is math.sin).
    #   Simply stay away from such test cases in this problem.
    # -------------------------------------------------------------------------
    print()
    print("--------------------------------------------------")
    print("Testing the   count_positive_sines   function:")
    print("--------------------------------------------------")

    # Test 1:
    expected = 4
    actual = count_positive_sines([3, 4, 5, 6, 7, 8, 9])
    print()
    print("Test 1 expected:", expected)
    print("       actual:  ", actual)

    # Test 2:
    expected = 2
    actual = count_positive_sines([3, 6, 8])
    print()
    print("Test 2 expected:", expected)
    print("       actual:  ", actual)

    # Test 3:
    expected = 3
    actual = count_positive_sines([7, 7, 7])
    print()
    print("Test 3 expected:", expected)
    print("       actual:  ", actual)

    # Test 4:
    expected = 0
    actual = count_positive_sines([6, 6, 6, 6, 6, 6, 6, 6, 6])
    print()
    print("Test 4 expected:", expected)
    print("       actual:  ", actual)

    # Test 5:
    expected = 1
    actual = count_positive_sines([3])
    print()
    print("Test 5 expected:", expected)
    print("       actual:  ", actual)

    # Test 6:
    expected = 0
    actual = count_positive_sines([6])
    print()
    print("Test 6 expected:", expected)
    print("       actual:  ", actual)

    # Test 7:
    expected = 0
    actual = count_positive_sines([5, 4, 6])
    print()
    print("Test 7 expected:", expected)
    print("       actual:  ", actual)

    # Test 8 (using the list [0, 1, 2, ... 1063]
    sequence = []
    for k in range(1064):
        sequence.append(k)

    expected = 532  # Trust me!
    actual = count_positive_sines(sequence)
    print()
    print("Test 8 expected:", expected)
    print("       actual:  ", actual)

    # Test 9 (using the list [0, 1, 2, ... 1064]
    sequence.append(1064)

    expected = 533  # Trust me!
    actual = count_positive_sines(sequence)
    print()
    print("Test 9 expected:", expected)
    print("       actual:  ", actual)

    # Test 10 (using the list [0, 1, 2, ... 1065]
    sequence.append(1065)

    expected = 533  # Trust me!
    actual = count_positive_sines(sequence)
    print()
    print("Test 10 expected:", expected)
    print("        actual:  ", actual)

    # TODO: 7 (continued):  Add your 1 ADDITIONAL test here:


def count_positive_sines(numbers):
    """
    What comes in:  An sequence of numbers.
    What goes out: Returns the number of items in the given sequence
      whose sine is positive.
    Side effects: None.

    Examples:  Since:
       sine(3) is about 0.14
       sine(4) is about -0.76
       sine(5) is about -0.96
       sine(6) is about -0.28
       sine(7) is about 0.66
       sine(8) is about 0.99
       sine(9) is about 0.41

      -- count_positive_sines([3, 4, 5, 6, 7, 8, 9])  returns 4
      -- count_positive_sines([3, 6, 8])              returns 2
      -- count_positive_sines([7, 7, 7])              returns 3

    Type hints:
      :type sequence: list or tuple (of numbers)
    """
    # -------------------------------------------------------------------------
    # TODO: 8. Implement and test this function.
    #   Note that you should write its TEST function first (above).
    # -------------------------------------------------------------------------


def run_test_sum_first_n():
    """ Tests the   sum_first_n   function. """
    # -------------------------------------------------------------------------
    # TODO: 9. Implement this TEST function.
    #   It TESTS the  sum_first_n  function defined below.
    #   Include at least ** 2 ** ADDITIONAL tests.
    #   ___
    #   As usual, include both EXPECTED and ACTUAL results in your test
    #   and compute the latter BY HAND (not by running your program).
    # -------------------------------------------------------------------------
    print()
    print("--------------------------------------------------")
    print("Testing the   sum_first_n   function:")
    print("--------------------------------------------------")

    # Test 1:
    expected = 0
    actual = sum_first_n([48, -10, 50, 5], 0)
    print()
    print("Test 1 expected:", expected)
    print("       actual:  ", actual)

    # Test 2:
    expected = 48
    actual = sum_first_n([48, -10, 50, 5], 1)
    print()
    print("Test 2 expected:", expected)
    print("       actual:  ", actual)

    # Test 3:
    expected = 38
    actual = sum_first_n([48, -10, 50, 5], 2)
    print()
    print("Test 3 expected:", expected)
    print("       actual:  ", actual)

    # Test 4:
    expected = 88
    actual = sum_first_n([48, -10, 50, 5], 3)
    print()
    print("Test 4 expected:", expected)
    print("       actual:  ", actual)

    # Test 5:
    expected = 93
    actual = sum_first_n([48, -10, 50, 5], 4)
    print()
    print("Test 5 expected:", expected)
    print("       actual:  ", actual)

    # Test 6:  This test uses a RANDOMLY generated sequence
    #          and an ORACLE to determine the expected (correct) result.
    sequence = []
    for _ in range(10000):
        sequence.append(random.randrange(-100, 100))
    expected = builtins.sum(sequence[:-1])
    actual = sum_first_n(sequence, 9999)
    print()
    print("Test 6 expected:", expected)
    print("       actual:  ", actual)

    # Test 7:  This test uses a RANDOMLY generated sequence
    #          and an ORACLE to determine the expected (correct) result.
    sequence = []
    for _ in range(10000):
        sequence.append(random.randrange(-100, 100))
    expected = builtins.sum(sequence[:-4000])
    actual = sum_first_n(sequence, 6000)
    print()
    print("Test 7 expected:", expected)
    print("       actual:  ", actual)

    # TODO: 9 (continued):  Add your 2 ADDITIONAL tests here:


def sum_first_n(numbers, n):
    """
    What comes in:
      -- An sequence of numbers.
      -- A nonnegative integer   n   that is less than or equal to
           the length of the given sequence.
    What goes out:
      Returns the sum of the first   n   numbers in the given sequence,
      where   n   is the second argument.
    Side effects: None.
    Examples:
      If numbers is   [48, -10, 50, 5], then:
      - sum_first_n(numbers, 0) returns 0  (the sum of NO numbers is 0)
      - sum_first_n(numbers, 1) returns 48
      - sum_first_n(numbers, 2) returns 38
      - sum_first_n(numbers, 3) returns 88
      - sum_first_n(numbers, 4) returns 93
    Type hints:
      :type numbers:   list of tuple (of numbers)
      :type n: int
    """
    # -------------------------------------------------------------------------
    # TODO: 10. Implement and test this function.
    #           Tests have been written for you (above).
    #  ___
    #  RESTRICTION:
    #    You may NOT use the built-in  sum   function
    #    in IMPLEMENTING this function.
    #      -- The TESTING code above does use   built_ins.sum
    #         as an ORACLE in TESTING this function, however.
    # -------------------------------------------------------------------------


# -----------------------------------------------------------------------------
# Calls  main  to start the ball rolling.
# -----------------------------------------------------------------------------
main()