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 = ['lab4.c']
    # Names of expected executables
    executables = ['lab4.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 lab4.c -o lab4.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(12.5)
    def test_Input1(self):
        # Title used by Gradescope 
        """Input/output test 1 (example from lab document)"""

        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab4.out < 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_noInitializing = open('reference/1noInitializing.txt', 'rb').read().strip().decode('utf-8')
                reference_withInitializing = open('reference/1withInitializing.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noInitializing = removeEmptyLines(reference_noInitializing)
                reference_withInitializing = removeEmptyLines(reference_withInitializing)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_withInitializing:
                    reference = reference_withInitializing
                else:
                    reference = reference_noInitializing
                
                # 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(12.5)
    def test_Input2(self):
        # Title used by Gradescope 
        """Input/output test 2"""
        
        checkExecutables(self, self.executables)

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["./lab4.out < 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_noInitializing = open('reference/2noInitializing.txt', 'rb').read().strip().decode('utf-8')
                reference_withInitializing = open('reference/2withInitializing.txt', 'rb').read().strip().decode('utf-8')
                
                # Remove empty lines from both output and reference
                stdout = removeEmptyLines(stdout)
                reference_noInitializing = removeEmptyLines(reference_noInitializing)
                reference_withInitializing = removeEmptyLines(reference_withInitializing)
                
                # Allow two different comparisons for minor white space differences
                if stdout == reference_withInitializing:
                    reference = reference_withInitializing
                else:
                    reference = reference_noInitializing
                
                # 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()