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

# Main unit test class
class TestDiff(unittest.TestCase):
    # Array of all the expected file names
    files = ['PA4.c', 'initList.c', 'buildItems.c', 'printList.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 student program (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)
        try:
            stdout, stderr = test.communicate(timeout=10)
        except (subprocess.TimeoutExpired):
            os.popen(removeLastExecutableCommand)
            kill_fail(test, self, compileTimeoutErrorMessage)
        
        # 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(5)
    def test_03test1Item(self):
        # Title used by Gradescope 
        """Program works correctly when only 1 item is entered"""

        executable = os.popen(findLastExecutableCommand).read()
        
        checkExecutables(self, executable.split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["$({}) 1 < input/1.txt".format(findLastExecutableCommand)], 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/1.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(15)
    def test_04test8ItemsPA4DocumentExample(self):
        # Title used by Gradescope 
        """Program works correctly when 8 items are entered (example from PA4 document)"""

        executable = os.popen(findLastExecutableCommand).read()
        
        checkExecutables(self, executable.split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["$({}) 8 < input/PA4DocumentExample_8.txt".format(findLastExecutableCommand)], 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/PA4DocumentExample_8.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(10)
    def test_05test15Items(self):
        # Title used by Gradescope 
        """Program works correctly when 15 items are entered"""

        executable = os.popen(findLastExecutableCommand).read()
        
        checkExecutables(self, executable.split())

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["$({}) 15 < input/15.txt".format(findLastExecutableCommand)], 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/15.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_06makeCleanTarget(self):
        # Title used by Gradescope 
        """make clean target works correctly"""
        
        checkExecutables(self, os.popen(findLastExecutableCommand).read().split())

        file = os.popen(findLastExecutableCommand).read()

        # Create a subprocess to run the student's code to obtain an output
        test = subprocess.Popen(["make clean"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        test.kill()
        
        if Path(file).is_file() or test.returncode:
            self.assertTrue(False, wrap("\"" + file + "\" was not removed by \"make clean\".", 65))
        else:
            self.assertTrue(True)
        
        test.terminate()