"""
Jason Yau 
12/06/2025

Use to quickly run one student source code from Webcourses on Eustis and output results using stdout.

====================================

Usage:

Go to assignments, P6, Download a .c source code submission.

Copy wordleheap.c file to temp Eustis folder like so (This assumes ~/temp/ exists already. If not, you will need to create the directory.): 
scp wordleheap.c <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

Copy this python script (p6_ind.py) to temp Eustis folder like so:
scp -r <path of p6_ind.py> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

Copy both test cases zip file to temp Eustis folder like so:
scp -r <path of p6-sample.zip> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/
scp -r <path of p6-secret.zip> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

ssh into eustis
ssh <YOUR_NID>@eustis.eecs.ucf.edu

Run the python script.
python3 ~/temp/p6_ind.py 1 # to print output and expected output of test case 1, etc.
python3 ~/temp/p6_ind.py 1 --keep_output_file # to print output and expected output of test case 1 and keep the output file, etc.

====================================

Make sure to delete all created files afterwards to be safe.
"""


import os
import glob
from zipfile import ZipFile
import shutil
import subprocess
import re
import sys
from collections import Counter
import time
    
points_per_case = 6
time_limit_seconds = 4

def grade_case(output: str, expected: str, case_input: str) -> int:
    out_lines = output.splitlines()
    out_lines = list(filter(lambda line: line and line.strip(), out_lines))
    exp_lines = expected.splitlines()
    if len(out_lines) != len(exp_lines):
        return 0
    matching_lines = 0
    for i in range(len(out_lines)):
        if out_lines[i] == exp_lines[i]:
            matching_lines += 1
    if matching_lines == len(out_lines):
        return points_per_case
    else:
        return 0

print_file = 1
if len(sys.argv) > 1:
    print_file = int(sys.argv[1])
keep_output_file = False
for arg in sys.argv:
    if arg.strip() == "--keep_output_file":
        keep_output_file = True

script_path = os.path.abspath(__file__)
script_directory = os.path.dirname(script_path)

extracted_path = f"{script_directory}/extracted"
if (len(glob.glob(extracted_path)) != 0):
    shutil.rmtree(extracted_path)

sample_cases_zip = glob.glob(f"{script_directory}/p6-sample.zip")
if len(sample_cases_zip) != 1:
    print(f"p6-sample.zip not found. Exiting.")
    exit(1)
with ZipFile(sample_cases_zip[0], 'r') as zip_object:
    zip_object.extractall(extracted_path)

secret_cases_zip = glob.glob(f"{script_directory}/p6-secret.zip")
if len(secret_cases_zip) != 1:
    print(f"p6-secret.zip not found. Exiting.")
    exit(1)
with ZipFile(secret_cases_zip[0], 'r') as zip_object:
    zip_object.extractall(extracted_path)

input_files = sorted(glob.glob(f"{extracted_path}/p6-*/wordleheap*.in"))
output_files = sorted(glob.glob(f"{extracted_path}/p6-*/wordleheap*.out"))
# Make sure we don't screw this part up.
assert len(input_files) == len(output_files)
for i in range(len(input_files)):
    assert input_files[i].split(".in")[0] in output_files[i].split(".out")[0]


source_code_files = glob.glob(f"{script_directory}/*.c")+glob.glob(f"{script_directory}/*.C")
if not (len(source_code_files) == 1):
    print(f"The number of .c source code files is not 1. Exiting...")
    exit(1)

comment = ""
total_case_points = 0

output = ""

for source_code_file in source_code_files:
    file_name = os.path.basename(source_code_file)
    case_points = 0
    passed_cases = 0
    

    executable = f"{source_code_file.split(".c")[0]}.exe"
    gcc_result = subprocess.run(["gcc", source_code_file, "-o", executable], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    compiled = gcc_result.returncode == 0

    if compiled:
        for i in range(len(input_files)):
            with open(input_files[i], "r") as fin:
                try:
                    start_time = time.perf_counter()
                    execute_result = subprocess.run([executable], stdin=fin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=time_limit_seconds, text=True, universal_newlines=True, encoding='mac_roman')
                    elapsed_time = time.perf_counter()-start_time
                    if i == print_file-1:
                        output = execute_result.stdout
                    if not execute_result.stdout or not execute_result.stdout.strip():
                        if execute_result.returncode != 0:
                            comment += f"Test case #{i+1}: FAILED (+0), reason: program crashed with exit code {execute_result.returncode}, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        else:
                            comment += f"Test case #{i+1}: FAILED (+0), reason: blank output, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        continue
                    curr_case_points = grade_case(execute_result.stdout, open(output_files[i], "r").read(), open(input_files[i], "r").read())
                    case_points += curr_case_points
                    if curr_case_points == points_per_case:
                        comment += f"Test case #{i+1}: PASSED (+{curr_case_points}), execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        passed_cases += 1
                    elif execute_result.returncode != 0:
                        comment += f"Test case #{i+1}: FAILED (+0), reason: program crashed with exit code {execute_result.returncode}, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                    else:
                        comment += f"Test case #{i+1}: FAILED (+0), reason: incorrect output, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                except subprocess.TimeoutExpired:
                    comment += f"Test case #{i+1}: FAILED (+0), reason: time limit exceeded, execution time: {time_limit_seconds:.4f}/{time_limit_seconds:.4f} seconds\n"
        os.remove(executable)
    else:
        comment += "Did not compile.\n"
    comment += f"\nOverall Points from Test cases: {case_points}/{len(input_files)*points_per_case}\n"
    comment += f"Overall Passed Test cases: {passed_cases}/{len(input_files)}\n"
    total_case_points += case_points


print("\n\nOutput:\n")
print(output)
if keep_output_file:
    with open("output.txt", "w") as output_file:
        output_file.write(output)
print("\n\nExpected Output:\n")
print(open(output_files[print_file-1], "r").read())

print("\n\nComment:\n")
print(comment)

shutil.rmtree(extracted_path)