import unittest
from gradescope_utils.autograder_utils.decorators import number, weight
import subprocess
from time import sleep
from re import sub
from utils import *
import signal
import re

def nonEmptyLine(s):
        return len(s.strip()) != 0
    
def stripstr(s):
    return s.strip()

def removeEmptyLines(text):
    lst = filter(nonEmptyLine, list(map(stripstr, text.split("\n"))))
    return "\n".join(list(lst))

class RuntimeAbort(Exception):
    pass

class RuntimeSegFault(Exception):
    pass

class RuntimeFPE(Exception):
    pass

class RuntimeBusError(Exception):
    pass

class RuntimeIllegalInstruction(Exception):
    pass

class MakefileError(Exception):
    pass

def checkRuntimeErrors(proc):
    if (proc.returncode == -signal.SIGABRT):
        raise RuntimeAbort
    elif (proc.returncode == -signal.SIGSEGV):
        raise RuntimeSegFault
    elif (proc.returncode == -signal.SIGFPE):
        raise RuntimeFPE
    elif (proc.returncode == -signal.SIGBUS):
        raise RuntimeBusError
    elif (proc.returncode == -signal.SIGILL):
        raise RuntimeIllegalInstruction
    elif (proc.returncode == 2):
        raise MakefileError

class TestDiff(unittest.TestCase):
    maxDiff = None
    
    # Array of all the expected file names
    files = ['makefile', 'mainDriver.c', 'getHeader.c', 'writeHeader.c', 'fillImageArray.c', 'writePixels.c', 'defs.h']
    
    def setUp(self):
        pass

    # Associated test number within Gradescope
    @number("0")
    # 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("1")
    # Associated point value within Gradescope
    @weight(5)
    def test_Compile(self):
        # Title used by Gradescope 
        """Clean compile"""

        # Create a subprocess to run the students make file to ensure it compiles
        test = subprocess.Popen(["make"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = test.stderr.read().strip().decode('utf-8')
        test.kill()

        # Standard unit test case with an associated error message
        self.longMessage = False
        self.assertTrue(output == "", msg=output)
        test.terminate()
        
    # Associated test number within Gradescope
    @number("2")
    # Associated point value within Gradescope
    @weight(10)
    def test_Header(self):
        # Title used by Gradescope 
        """Check that PNM header information is correct"""
        
        # Boolean to hold value for test pass/fail (updated procedurally)
        correct = True

        # Create a subprocess to run the students code to obtain an output
        test = subprocess.Popen(["make -s run"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test)
            
            try:
                output = stdout.strip().decode('utf-8')
                test.kill()

                # Set msg to blank string, in case test passes
                msg = ''
                
                # Array of expected lines of output, line by line in expected order
                expected = ['P3', '640', '360', '255']
                
                # Check if header information is correct
                header = output.split()
                try:
                    if header[0] != expected[0]:
                        correct = False
                        msg = [f'Your PPM image\'s header label is incorrect. Check the Lab 6 instructions for the correct file format label. Your header laber: {header[0]}, expected header label: {expected[0]}']
                    elif header[1] != expected[1]:
                        correct = False
                        msg = [f'Your PPM image\'s width is incorrect. Make sure your program uses the width entered by the user for your PPM image. Your width: {header[1]}, expected width: {expected[1]}']
                    elif header[2] != expected[2]:
                        correct = False
                        msg = [f'Your PPM image\'s height is incorrect. Check the Lab 6 instructions for where to find the correct height. Your height: {header[2]}, expected height: {expected[2]}']
                    elif header[3] != expected[3]:
                        correct = False
                        msg = [f'Your PPM image\'s maximum pixel value is incorrect. Check the Lab 6 instructions for the maximum pixel value of a PPM image. Your value: {header[3]}, expected value: {expected[3]}']
                except (IndexError):
                    correct = False
                    msg = ['Your PPM image\'s header is too short, and cannot be used in autograder comparisons. Ensure your program prints the correct header information.']
            except (UnicodeDecodeError):
                correct = False
                msg = ['Your program printed a character that the autograder cannot decode. Ensure your program prints valid characters.']
        except (RuntimeAbort):
            correct = False
            msg = ['Your program triggered runtime error SIGABRT. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeSegFault):
            correct = False
            msg = ['Your program encountered a segmentation fault. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeFPE):
            correct = False
            msg = ['Your program triggered runtime error SIGFPE (typically caused by dividing by zero). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeBusError):
            correct = False
            msg = ['Your program triggered runtime error SIGBUS. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeIllegalInstruction):
            correct = False
            msg = ['Your program triggered runtime error SIGILL (typically caused by stack smashing). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (MakefileError):
            correct = False
            msg = (stderr.strip().decode('utf-8') + '\n' + stdout.strip().decode('utf-8')).split('\n')
        
        # Pass test pass/fail status and message to test run output
        self.longMessage = False
        self.assertTrue(correct, msg)
        test.terminate()
        
    # Associated test number within Gradescope
    @number("3")
    # Associated point value within Gradescope
    @weight(10)
    def test_FirstPixel(self):
        # Title used by Gradescope 
        """Check that first pixel is correct"""
        
        # Boolean to hold value for test pass/fail (updated procedurally)
        correct = True

        # Create a subprocess to run the students code to obtain an output
        test = subprocess.Popen(["make -s run"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test)
        
            try:
                output = stdout.strip().decode('utf-8')
                test.kill()

                # Set msg to blank string, in case test passes
                msg = ''
                
                # Array of expected lines of output, line by line in expected order
                expected = [('203', '226', '242')]
                
                try:
                    # Split program output from header information to pixels
                    encoding, height, width, max, *values = output.split()
                    # Create array of PPM pixels
                    rgb = [tuple(values[i:i+3]) for i in range(0, len(values), 3)]
                    
                    try:
                        # Check that first pixel in image is correct
                        if rgb[0] != expected[0]:
                            correct = False
                            msg = [f'The first pixel in your PPM image is incorrect. Your pixel: {rgb[0]}, expected pixel: {expected[0]}']
                    except (IndexError):
                        correct = False
                        msg = ['Your PPM image doesn\'t print any pixels, and can\'t be used in comparisons. Ensure your program prints at least one pixel.']
                except (ValueError):
                    correct = False
                    msg = ['Your PPM image doesn\'t print any pixels, and can\'t be used in comparisons. Ensure your program prints at least one pixel.']
            except (UnicodeDecodeError):
                correct = False
                msg = ['Your program printed a character that the autograder cannot decode. Ensure your program prints valid characters.']
        except (RuntimeAbort):
            correct = False
            msg = ['Your program triggered runtime error SIGABRT. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeSegFault):
            correct = False
            msg = ['Your program encountered a segmentation fault. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeFPE):
            correct = False
            msg = ['Your program triggered runtime error SIGFPE (typically caused by dividing by zero). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeBusError):
            correct = False
            msg = ['Your program triggered runtime error SIGBUS. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeIllegalInstruction):
            correct = False
            msg = ['Your program triggered runtime error SIGILL (typically caused by stack smashing). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (MakefileError):
            correct = False
            msg = (stderr.strip().decode('utf-8') + '\n' + stdout.strip().decode('utf-8')).split('\n')
        
        # Pass test pass/fail status and message to test run output
        self.longMessage = False
        self.assertTrue(correct, msg)
        test.terminate()
        
    # Associated test number within Gradescope
    @number("4")
    # Associated point value within Gradescope
    @weight(10)
    def test_LastPixel(self):
        # Title used by Gradescope 
        """Check that last pixel is correct"""
        
        # Boolean to hold value for test pass/fail (updated procedurally)
        correct = True

        # Create a subprocess to run the students code to obtain an output
        test = subprocess.Popen(["make -s run"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test)
            
            try:
                output = stdout.strip().decode('utf-8')
                test.kill()

                # Set msg to blank string, in case test passes
                msg = ''
                
                # Array of expected lines of output, line by line in expected order
                expected1 = [('101', '102', '94')]
                expected2 = [('202', '205', '188')]
                expected3 = [('134', '157', '163')]
                expected4 = [('116', '144', '148')]
                expected5 = [('76', '85', '88')]
                expected6 = [('82', '107', '114')]
                expected7 = [('144', '165', '170')]
                
                try:
                    # Split program output from header information to pixels
                    encoding, height, width, max, *values = output.split()
                    # Create array of PPM pixels
                    rgb = [tuple(values[i:i+3]) for i in range(0, len(values), 3)]
                
                    try:
                        # Check that last pixel in image is correct
                        if rgb[len(rgb) - 1] != expected1[0] and rgb[len(rgb) - 1] != expected2[0] and rgb[len(rgb) - 1] != expected3[0] and rgb[len(rgb) - 1] != expected4[0] and rgb[len(rgb) - 1] != expected5[0] and rgb[len(rgb) - 1] != expected6[0] and rgb[len(rgb) - 1] != expected7[0]:
                            correct = False
                            msg = [f'The last pixel in your PPM image is incorrect. Your pixel: {rgb[len(rgb) - 1]}, accepted expected pixels: {expected1[0]}, {expected2[0]}, {expected3[0]}, {expected4[0]}, {expected5[0]}, {expected6[0]}, {expected7[0]}']
                    except (IndexError):
                        correct = False
                        msg = ['Your PPM image doesn\'t print any pixels, and can\'t be used in comparisons. Ensure your program prints at least one pixel.']
                except (ValueError):
                    correct = False
                    msg = ['Your PPM image doesn\'t print any pixels, and can\'t be used in comparisons. Ensure your program prints at least one pixel.']
            except (UnicodeDecodeError):
                correct = False
                msg = ['Your program printed a character that the autograder cannot decode. Ensure your program prints valid characters.']
        except (RuntimeAbort):
            correct = False
            msg = ['Your program triggered runtime error SIGABRT. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeSegFault):
            correct = False
            msg = ['Your program encountered a segmentation fault. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeFPE):
            correct = False
            msg = ['Your program triggered runtime error SIGFPE (typically caused by dividing by zero). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeBusError):
            correct = False
            msg = ['Your program triggered runtime error SIGBUS. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeIllegalInstruction):
            correct = False
            msg = ['Your program triggered runtime error SIGILL (typically caused by stack smashing). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (MakefileError):
            correct = False
            msg = (stderr.strip().decode('utf-8') + '\n' + stdout.strip().decode('utf-8')).split('\n')
        
        # Pass test pass/fail status and message to test run output
        self.longMessage = False
        self.assertTrue(correct, msg)
        test.terminate()
        
    # Associated test number within Gradescope
    @number("5")
    # Associated point value within Gradescope
    @weight(10)
    def test_NumPixels(self):
        # Title used by Gradescope 
        """Check that PNM image has correct number of pixels"""
        
        # Boolean to hold value for test pass/fail (updated procedurally)
        correct = True

        # Create a subprocess to run the students code to obtain an output
        test = subprocess.Popen(["make -s run"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = test.communicate()
        
        try:
            checkRuntimeErrors(test)
            
            try:
                output = stdout.strip().decode('utf-8')
                test.kill()

                # Set msg to blank string, in case test passes
                msg = ''
                
                # Check that length of rgb array is correct, if not then fail test and set appropriate error
                try:
                    # Split program output from header information to pixels
                    encoding, height, width, max, *values = output.split()
                    # Create array of PPM pixels
                    rgb = [tuple(values[i:i+3]) for i in range(0, len(values), 3)]
                    
                    if len(rgb) < 230400:
                        correct = False
                        msg = [f'Your PPM image contains fewer pixels than expected. Your length: {len(rgb)} pixels, expected length: 230,400 pixels']
                    elif len(rgb) > 230400:
                        correct = False
                        msg = [f'Your PPM image contains more pixels than expected. Your length: {len(rgb)} pixels, expected length: 230,400 pixels']
                except (ValueError):
                    correct = False
                    msg = ['Your PPM image doesn\'t print any pixels, and can\'t be used in comparisons. Ensure your program prints at least one pixel.']
            except (UnicodeDecodeError):
                correct = False
                msg = ['Your program printed a character that the autograder cannot decode. Ensure your program prints valid characters.']
        except (RuntimeAbort):
            correct = False
            msg = ['Your program triggered runtime error SIGABRT. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeSegFault):
            correct = False
            msg = ['Your program encountered a segmentation fault. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeFPE):
            correct = False
            msg = ['Your program triggered runtime error SIGFPE (typically caused by dividing by zero). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeBusError):
            correct = False
            msg = ['Your program triggered runtime error SIGBUS. Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (RuntimeIllegalInstruction):
            correct = False
            msg = ['Your program triggered runtime error SIGILL (typically caused by stack smashing). Check for compilation warnings, use GDB to track down the cause of this error, or Google this error for more information.']
        except (MakefileError):
            correct = False
            msg = (stderr.strip().decode('utf-8') + '\n' + stdout.strip().decode('utf-8')).split('\n')
        
        # Pass test pass/fail status and message to test run output
        self.longMessage = False
        self.assertTrue(correct, msg)
        test.terminate()