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

# Main unit test class
class TestDiff(unittest.TestCase):
    # Array of all the expected file names
    files = ['lab8.c']
    # Names of expected executables
    executables = ['lab8.out']
    
    # 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_checkFiles(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_Compile(self):
        # Title used by Gradescope 
        """Clean compile"""

        checkSourceFiles(self, self.files)

        # Create a subprocess to run the student's Makefile to ensure it compiles
        test = subprocess.Popen(["gcc -Wall lab8.c -o lab8.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("3")
    # 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(2.5)
    def test_EnforceValidMenuInput(self):
        # Title used by Gradescope 
        """Program enforces valid menu input"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/enforceValidMenuInput.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 = open('reference/enforceValidMenuInput.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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("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(5)
    def test_PrintPoem(self):
        # Title used by Gradescope 
        """Program prints poem"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/printPoem.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 = open('reference/printPoem.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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(5)
    def test_NumLines(self):
        # Title used by Gradescope 
        """Program prints number of lines in poem"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/numLines.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 = open('reference/numLines.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(5)
    def test_ConvertCase(self):
        # Title used by Gradescope 
        """Program prints poem with converted case"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/convertCase.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 = open('reference/convertCase.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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("7")
    # 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(2.5)
    def test_QuitImmediately(self):
        # Title used by Gradescope 
        """Program quits immediately"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/quitImmediately.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 = open('reference/quitImmediately.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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("visible")
    # Individual test case timeout (in seconds)
    @timeout.timeout(10, exception_message=wrap(programTimeoutErrorMessage, 65), use_signals=False)
    # Associated point value within Gradescope
    @weight(5)
    def test_DocumentExample(self):
        # Title used by Gradescope 
        """Example run from lab document (carried over from Lab 7)"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab8.out < input/documentExample.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 = open('reference/documentExample.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference = removeEmptyLines(reference)
                
                # 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()