import unittest
# timeout.py
import timeout
# Requires gradescope_utils
from gradescope_utils.autograder_utils.decorators import number, weight, visibility, hide_errors
import subprocess
from time import sleep
from re import sub
# utils.py
from utils import *
import os
import shutil

# Main unit test class
class TestDiff(unittest.TestCase):
    # Array of all the expected file names
    files = ['lab9.c', 'arrayProcessing.c', 'defs.h', 'makefile']
    
    # Set up unittest environment
    def setUp(self):
        self.maxDiff = None
        self.longMessage = False
        self.addTypeEqualityFunc(str, self.customCompare)
        
    # Define custom TypeEquality function that calls function from utils.py
    def customCompare(self, first, second, msg=None):
        customAssertMultiLineEqual(self, first, second, msg)

    # Associated test number within Gradescope
    @number("1")
    # Test visibility
    @visibility("visible")
    # Associated point value within Gradescope
    @weight(0)
    def test_01checkFiles(self):
        """Ensure all required files are present"""
        
        checkFiles(self.files)
        sleep(1)
    
    # Associated test number within Gradescope
    @number("2")
    # Test visibility
    @visibility("visible")
    # Associated point value within Gradescope
    @weight(5)
    def test_02CompileMain(self):
        # Title used by Gradescope 
        """Clean compile (using makefile)"""

        checkSourceFiles(self, self.files)

        # Create a subprocess to run the student's Makefile to ensure it compiles
        test = subprocess.Popen(["make"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        # Try to decode stderr
        try:
            stderr = stderr.strip().decode('utf-8')
            test.kill()
            
            self.assertTrue(stderr == "", msg=("See compiler output:\n" + stderr))
            
        # Catch exception for decode error
        except (UnicodeDecodeError):
            kill_fail(test, self, compileDecodeErrorMessage)
        
        test.terminate()
    
    # Associated test number within Gradescope
    @number("3")
    # Test visibility
    @visibility("hidden")
    # Associated point value within Gradescope
    @weight(0)
    def test_03CopyExercises1(self):
        # Title used by Gradescope 
        """Replace exercises.txt with exercises1.txt (debug only)"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        shutil.copyfile('exercises1.txt', 'exercises.txt')

    # Associated test number within Gradescope
    @number("4")
    # Test visibility
    @visibility("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(15)
    def test_04Exercises1Input1(self):
        # Title used by Gradescope 
        """Example test run (from lab document)"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["make -s run < input/1.txt"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stdout = checkForUninitializedChars(stdout.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference_noNewLines = open('reference/exercises1out1_noNewLines.txt', 'rb').read().strip().decode('utf-8')
                reference_withNewLines = open('reference/exercises1out1_withNewLines.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noNewLines = removeEmptyLines(reference_noNewLines)
                reference_withNewLines = removeEmptyLines(reference_withNewLines)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_noNewLines:
                    reference = reference_noNewLines
                else:
                    reference = reference_withNewLines
                
                # Check the contents of stdout against reference
                self.customCompare(stdout, reference, msg='Program output does not match expected output')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()

    # Associated test number within Gradescope
    @number("5")
    # Test visibility
    @visibility("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(15)
    def test_05Exercises1Input2(self):
        # Title used by Gradescope 
        """Test with different input for workout stats"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["make -s run < input/2.txt"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stdout = checkForUninitializedChars(stdout.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference_noNewLines = open('reference/exercises1out2_noNewLines.txt', 'rb').read().strip().decode('utf-8')
                reference_withNewLines = open('reference/exercises1out2_withNewLines.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noNewLines = removeEmptyLines(reference_noNewLines)
                reference_withNewLines = removeEmptyLines(reference_withNewLines)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_noNewLines:
                    reference = reference_noNewLines
                else:
                    reference = reference_withNewLines
                
                # Check the contents of stdout against reference
                self.customCompare(stdout, reference, msg='Program output does not match expected output')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()
    
    # Associated test number within Gradescope
    @number("6")
    # Test visibility
    @visibility("hidden")
    # Associated point value within Gradescope
    @weight(0)
    def test_06CopyExercises2(self):
        # Title used by Gradescope 
        """Replace exercises.txt with exercises2.txt (debug only)"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        shutil.copyfile('exercises2.txt', 'exercises.txt')

    # Associated test number within Gradescope
    @number("7")
    # Test visibility
    @visibility("after_due_date")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(10)
    def test_07Exercises2Input1(self):
        # Title used by Gradescope 
        """Test with different set of workout data"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["make -s run < input/1.txt"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stdout = checkForUninitializedChars(stdout.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference_noNewLines = open('reference/exercises2out1_noNewLines.txt', 'rb').read().strip().decode('utf-8')
                reference_withNewLines = open('reference/exercises2out1_withNewLines.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noNewLines = removeEmptyLines(reference_noNewLines)
                reference_withNewLines = removeEmptyLines(reference_withNewLines)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_noNewLines:
                    reference = reference_noNewLines
                else:
                    reference = reference_withNewLines
                
                # Check the contents of stdout against reference
                self.customCompare(stdout, reference, msg='Program output does not match expected output')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()

    # Associated test number within Gradescope
    @number("8")
    # Test visibility
    @visibility("after_due_date")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(10)
    def test_08Exercises2Input2(self):
        # Title used by Gradescope 
        """Test with combination of different workout data stats"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["make -s run < input/2.txt"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stdout = checkForUninitializedChars(stdout.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference_noNewLines = open('reference/exercises2out2_noNewLines.txt', 'rb').read().strip().decode('utf-8')
                reference_withNewLines = open('reference/exercises2out2_withNewLines.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noNewLines = removeEmptyLines(reference_noNewLines)
                reference_withNewLines = removeEmptyLines(reference_withNewLines)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_noNewLines:
                    reference = reference_noNewLines
                else:
                    reference = reference_withNewLines
                
                # Check the contents of stdout against reference
                self.customCompare(stdout, reference, msg='Program output does not match expected output')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()
    
    # Associated test number within Gradescope
    @number("9")
    # Test visibility
    @visibility("hidden")
    # Associated point value within Gradescope
    @weight(0)
    def test_09CopyExercises1(self):
        # Title used by Gradescope 
        """Replace exercises.txt with exercises1.txt (again, for unit tests) (debug only)"""

        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        shutil.copyfile('exercises1.txt', 'exercises.txt')
        
    # Associated test number within Gradescope
    @number("10")
    # Test visibility
    @visibility("after_due_date")
    # Associated point value within Gradescope
    @weight(2.5)
    def test_10CompileInitArrayTest(self):
        # Title used by Gradescope 
        """Compile unit test for initArray"""

        checkSourceFiles(self, self.files)

        # Create a subprocess to run the student's Makefile to ensure it compiles
        test = subprocess.Popen(["gcc -Wall initArrayDriver.c arrayProcessing.c -o initArrayTest.out"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        # Try to decode stderr
        try:
            stderr = stderr.strip().decode('utf-8')
            test.kill()
            
            self.assertTrue(stderr == "", msg=("See compiler output:\n" + stderr))
            
        # Catch exception for decode error
        except (UnicodeDecodeError):
            kill_fail(test, self, compileDecodeErrorMessage)
        
        test.terminate()

    # Associated test number within Gradescope
    @number("11")
    # Test visibility
    @visibility("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(7.5)
    # Custom error message override (so as not to show expected unit test output)
    @hide_errors(wrap("Test Failed: initArray function did not successfully scan in the correct exercise names and/or muscle groups. Ensure your initArray function accepts the correct number of inputs and stores them in the appropriate struct members.", 65))
    def test_11InitArrayUnitTest(self):
        # Title used by Gradescope 
        """initArray function unit test"""

        checkExecutables(self, ['initArrayTest.out'])

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./initArrayTest.out"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stderr = checkForUninitializedChars(stderr.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference = open('reference/initArrayUnitTest.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stderr = removeEmptyLines(stderr)
                reference = removeEmptyLines(reference)
                
                # Check the contents of stdout against reference
                self.customCompare(stderr, reference, msg='Unit test failed')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()
        
    # Associated test number within Gradescope
    @number("12")
    # Test visibility
    @visibility("after_due_date")
    # Associated point value within Gradescope
    @weight(2.5)
    def test_12CompileGetWorkoutTest(self):
        # Title used by Gradescope 
        """Compile unit test for getWorkout"""

        checkSourceFiles(self, self.files)

        # Create a subprocess to run the student's Makefile to ensure it compiles
        test = subprocess.Popen(["gcc -Wall getWorkoutDriver.c arrayProcessing.c -o getWorkoutTest.out"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        # Try to decode stderr
        try:
            stderr = stderr.strip().decode('utf-8')
            test.kill()
            
            self.assertTrue(stderr == "", msg=("See compiler output:\n" + stderr))
            
        # Catch exception for decode error
        except (UnicodeDecodeError):
            kill_fail(test, self, compileDecodeErrorMessage)
        
        test.terminate()

    # Associated test number within Gradescope
    @number("13")
    # Test visibility
    @visibility("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(7.5)
    # Custom error message override (so as not to show expected unit test output)
    @hide_errors(wrap("Test Failed: getWorkout function did not successfully scan in the correct values for exercise weight, time, sets, and/or reps. Ensure your getWorkout function prompts for certain inputs (such as weight or time) for a given exercise according to the lab instructions.", 65))
    def test_13GetWorkoutUnitTest(self):
        # Title used by Gradescope 
        """getWorkout function unit test"""

        checkExecutables(self, ['getWorkoutTest.out'])

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./getWorkoutTest.out < input/getWorkoutUnitTest.txt"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test, self, stdout, stderr)
            
            # Try to decode stdout
            try:
                stderr = checkForUninitializedChars(stderr.strip().decode('utf-8'))
                test.kill()
                
                # Open reference output and decode
                reference = open('reference/getWorkoutUnitTest.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stderr = removeEmptyLines(stderr)
                reference = removeEmptyLines(reference)
                
                # Check the contents of stdout against reference
                self.customCompare(stderr, reference, msg='Unit test failed')
            
            # Catch exception for decode error
            except (UnicodeDecodeError):
                kill_fail(test, self, decodeErrorMessage)
                
            # Catch exception for uninitialized characters
            except (UninitializedCharError):
                kill_fail(test, self, uninitializedCharacterMessage)
        
        # Catch runtime error exceptions
        except (RuntimeAbort, RuntimeSegFault, RuntimeFPE, RuntimeBusError, RuntimeIllegalInstruction, MakefileError):
            pass
        
        test.terminate()