import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.DefaultCaret;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class BakeTestRunner extends JFrame {
    private JPanel mainPanel;
    private JTable testTable;
    private DefaultTableModel tableModel;
    private JTextArea logArea;
    private JButton runSelectedButton;
    private JButton runAllButton;
    private JComboBox<String> languageComboBox;
    private JProgressBar progressBar;
    private JButton openLogsButton;
    private JButton compareSelectedButton;
    
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final String baseDir = System.getProperty("user.dir");
    private final String logsDir = baseDir + File.separator + "logs";
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    private final Color PASSED_COLOR = new Color(220, 255, 220);
    private final Color FAILED_COLOR = new Color(255, 220, 220);
    private final Color RUNNING_COLOR = new Color(220, 220, 255);
    
    enum TestStatus {
        NOT_RUN, RUNNING, PASSED, FAILED
    }
    
    public BakeTestRunner() {
        setTitle("Bake Test Runner");
        setSize(1000, 700);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        
        setupUI();
        loadTests();
        createLogsDirectory();
    }
    
    private void setupUI() {
        mainPanel = new JPanel(new BorderLayout(10, 10));
        mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
        
        // Top panel with controls
        JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
        runSelectedButton = new JButton("Run Selected Tests");
        runAllButton = new JButton("Run All Tests");
        compareSelectedButton = new JButton("Compare Selected Test");
        languageComboBox = new JComboBox<>(new String[]{"All", "C", "Java"});
        openLogsButton = new JButton("Open Logs Directory");
        
        controlPanel.add(runSelectedButton);
        controlPanel.add(runAllButton);
        controlPanel.add(compareSelectedButton);
        controlPanel.add(new JLabel("Language:"));
        controlPanel.add(languageComboBox);
        controlPanel.add(openLogsButton);
        
        // Table for test list
        String[] columnNames = {"#", "Language", "Test Name", "Status", "Last Run"};
        tableModel = new DefaultTableModel(columnNames, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };
        testTable = new JTable(tableModel);
        testTable.getColumnModel().getColumn(0).setPreferredWidth(30);
        testTable.getColumnModel().getColumn(1).setPreferredWidth(70);
        testTable.getColumnModel().getColumn(2).setPreferredWidth(300);
        testTable.getColumnModel().getColumn(3).setPreferredWidth(80);
        testTable.getColumnModel().getColumn(4).setPreferredWidth(150);
        testTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        testTable.setRowHeight(25);
        
        // Log area
        logArea = new JTextArea();
        logArea.setEditable(false);
        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        DefaultCaret caret = (DefaultCaret) logArea.getCaret();
        caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
        
        // Progress bar
        progressBar = new JProgressBar(0, 100);
        progressBar.setStringPainted(true);
        progressBar.setString("Ready");
        
        // Layout
        JSplitPane splitPane = new JSplitPane(
            JSplitPane.VERTICAL_SPLIT,
            new JScrollPane(testTable),
            new JScrollPane(logArea)
        );
        splitPane.setDividerLocation(300);
        
        mainPanel.add(controlPanel, BorderLayout.NORTH);
        mainPanel.add(splitPane, BorderLayout.CENTER);
        mainPanel.add(progressBar, BorderLayout.SOUTH);
        
        setContentPane(mainPanel);
        
        // Add action listeners
        runSelectedButton.addActionListener(e -> runSelectedTests());
        runAllButton.addActionListener(e -> runAllTests());
        compareSelectedButton.addActionListener(e -> compareSelectedTest());
        openLogsButton.addActionListener(e -> openLogsDirectory());
        languageComboBox.addActionListener(e -> filterTestsByLanguage());
    }
    
    private void createLogsDirectory() {
        try {
            Files.createDirectories(Paths.get(logsDir));
        } catch (IOException e) {
            logMessage("Error creating logs directory: " + e.getMessage());
        }
    }
    
    private void openLogsDirectory() {
        try {
            Desktop.getDesktop().open(new File(logsDir));
        } catch (IOException e) {
            logMessage("Error opening logs directory: " + e.getMessage());
            JOptionPane.showMessageDialog(this, 
                "Could not open logs directory: " + e.getMessage(), 
                "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private void compareSelectedTest() {
        int selectedRow = testTable.getSelectedRow();
        if (selectedRow == -1) {
            JOptionPane.showMessageDialog(this, 
                "Please select a test to compare", 
                "No Test Selected", JOptionPane.WARNING_MESSAGE);
            return;
        }
        
        String language = (String) tableModel.getValueAt(selectedRow, 1);
        String testName = (String) tableModel.getValueAt(selectedRow, 2);
        
        String logFilePath = logsDir + File.separator + language + "_" + testName + ".log";
        File logFile = new File(logFilePath);
        
        if (!logFile.exists()) {
            JOptionPane.showMessageDialog(this, 
                "No log file found for this test. Please run the test first.", 
                "Log Not Found", JOptionPane.WARNING_MESSAGE);
            return;
        }
        
        showComparisonDialog(logFile, language, testName);
    }
    
    private void showComparisonDialog(File logFile, String language, String testName) {
        JDialog dialog = new JDialog(this, "Comparison: " + language + " - " + testName, true);
        dialog.setLayout(new BorderLayout(10, 10));
        dialog.setSize(1000, 600);
        dialog.setLocationRelativeTo(this);
        
        try {
            List<String> lines = Files.readAllLines(logFile.toPath());
            String content = String.join("\n", lines);
            
            // Split content to make and bake sections if possible
            String makeOutput = "";
            String bakeOutput = "";
            
            // Basic parsing - can be enhanced for better splitting
            int makeIndex = content.indexOf("=== Make Output ===");
            int bakeIndex = content.indexOf("=== Bake Output ===");
            int comparisonIndex = content.indexOf("=== Comparison Results ===");
            
            if (makeIndex != -1 && bakeIndex != -1) {
                makeOutput = content.substring(makeIndex, bakeIndex).trim();
                if (comparisonIndex != -1) {
                    bakeOutput = content.substring(bakeIndex, comparisonIndex).trim();
                } else {
                    bakeOutput = content.substring(bakeIndex).trim();
                }
            }
            
            JTextArea makeArea = new JTextArea(makeOutput);
            JTextArea bakeArea = new JTextArea(bakeOutput);
            JTextArea comparisonArea = new JTextArea();
            
            if (comparisonIndex != -1) {
                comparisonArea.setText(content.substring(comparisonIndex).trim());
            } else {
                comparisonArea.setText("No comparison results available.");
            }
            
            makeArea.setEditable(false);
            bakeArea.setEditable(false);
            comparisonArea.setEditable(false);
            
            makeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            bakeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            comparisonArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            
            JTabbedPane tabbedPane = new JTabbedPane();
            tabbedPane.addTab("Make Output", new JScrollPane(makeArea));
            tabbedPane.addTab("Bake Output", new JScrollPane(bakeArea));
            tabbedPane.addTab("Comparison", new JScrollPane(comparisonArea));
            
            dialog.add(tabbedPane, BorderLayout.CENTER);
            
            JButton closeButton = new JButton("Close");
            closeButton.addActionListener(e -> dialog.dispose());
            
            JPanel buttonPanel = new JPanel();
            buttonPanel.add(closeButton);
            dialog.add(buttonPanel, BorderLayout.SOUTH);
            
            dialog.setVisible(true);
            
        } catch (IOException e) {
            JOptionPane.showMessageDialog(dialog, 
                "Error reading log file: " + e.getMessage(), 
                "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private void loadTests() {
        tableModel.setRowCount(0);
        
        // Load C tests
        loadTestsForLanguage("C");
        
        // Load Java tests
        loadTestsForLanguage("Java");
    }
    
    private void loadTestsForLanguage(String language) {
        File languageDir = new File(baseDir + File.separator + "tests" + File.separator + language);
        if (!languageDir.exists() || !languageDir.isDirectory()) {
            logMessage("Warning: Directory not found: " + languageDir.getPath());
            return;
        }
        
        File[] testDirs = languageDir.listFiles(File::isDirectory);
        if (testDirs == null) {
            logMessage("Warning: No test directories found in " + languageDir.getPath());
            return;
        }
        
        Arrays.sort(testDirs, (a, b) -> {
            // Extract test number for sorting
            Pattern pattern = Pattern.compile("test-(\\d+)");
            Matcher matcherA = pattern.matcher(a.getName());
            Matcher matcherB = pattern.matcher(b.getName());
            
            if (matcherA.find() && matcherB.find()) {
                try {
                    int numA = Integer.parseInt(matcherA.group(1));
                    int numB = Integer.parseInt(matcherB.group(1));
                    return Integer.compare(numA, numB);
                } catch (NumberFormatException e) {
                    return a.getName().compareTo(b.getName());
                }
            }
            return a.getName().compareTo(b.getName());
        });
        
        for (File testDir : testDirs) {
            String testName = testDir.getName();
            if (testName.startsWith("test-")) {
                Object[] row = {tableModel.getRowCount() + 1, language, testName, "Not Run", ""};
                tableModel.addRow(row);
            }
        }
    }
    
    private void filterTestsByLanguage() {
        String selectedLanguage = (String) languageComboBox.getSelectedItem();
        if (selectedLanguage == null || selectedLanguage.equals("All")) {
            loadTests();
            return;
        }
        
        tableModel.setRowCount(0);
        loadTestsForLanguage(selectedLanguage);
    }
    
    private void runSelectedTests() {
        int[] selectedRows = testTable.getSelectedRows();
        if (selectedRows.length == 0) {
            JOptionPane.showMessageDialog(this, 
                "Please select at least one test to run", 
                "No Test Selected", JOptionPane.WARNING_MESSAGE);
            return;
        }
        
        List<TestInfo> testsToRun = new ArrayList<>();
        for (int row : selectedRows) {
            String language = (String) tableModel.getValueAt(row, 1);
            String testName = (String) tableModel.getValueAt(row, 2);
            testsToRun.add(new TestInfo(row, language, testName));
        }
        
        disableButtons();
        runTests(testsToRun);
    }
    
    private void runAllTests() {
        List<TestInfo> testsToRun = new ArrayList<>();
        for (int row = 0; row < tableModel.getRowCount(); row++) {
            String language = (String) tableModel.getValueAt(row, 1);
            String testName = (String) tableModel.getValueAt(row, 2);
            testsToRun.add(new TestInfo(row, language, testName));
        }
        
        disableButtons();
        runTests(testsToRun);
    }
    
    private void disableButtons() {
        runSelectedButton.setEnabled(false);
        runAllButton.setEnabled(false);
        compareSelectedButton.setEnabled(false);
        languageComboBox.setEnabled(false);
    }
    
    private void enableButtons() {
        runSelectedButton.setEnabled(true);
        runAllButton.setEnabled(true);
        compareSelectedButton.setEnabled(true);
        languageComboBox.setEnabled(true);
    }
    
    private void runTests(List<TestInfo> testsToRun) {
        progressBar.setValue(0);
        progressBar.setString("Running tests (0/" + testsToRun.size() + ")");
        logArea.setText("");
        
        executor.submit(() -> {
            try {
                int total = testsToRun.size();
                int current = 0;
                
                for (int i = 0; i < testsToRun.size(); i++) {
                    TestInfo test = testsToRun.get(i);
                    final int currentTest = i + 1;
                    
                    // Update UI to show we're running this test
                    SwingUtilities.invokeLater(() -> {
                        tableModel.setValueAt(TestStatus.RUNNING.name(), test.row, 3);
                        testTable.setValueAt(TestStatus.RUNNING.name(), test.row, 3);
                        testTable.setValueAt(dateFormat.format(new Date()), test.row, 4);
                        
                        // Highlight the row
                        testTable.setRowSelectionInterval(test.row, test.row);
                        
                        // Update the progress bar
                        progressBar.setValue((int)((double)currentTest / total * 100));
                        progressBar.setString("Running tests (" + currentTest + "/" + total + ")");
                    });
                    
                    // Run the test
                    logMessage("\n========================================================");
                    logMessage("Running Test: " + test.language + " - " + test.testName);
                    logMessage("========================================================");
                    
                    boolean success = runTest(test);
                    
                    // Update UI with the result
                    SwingUtilities.invokeLater(() -> {
                        testTable.setValueAt(success ? TestStatus.PASSED.name() : TestStatus.FAILED.name(), 
                                         test.row, 3);
                    });
                }
                
                // Test run complete
                SwingUtilities.invokeLater(() -> {
                    progressBar.setValue(100);
                    progressBar.setString("All tests completed");
                    enableButtons();
                    logMessage("\n========================================================");
                    logMessage("Test run completed at " + dateFormat.format(new Date()));
                    logMessage("========================================================");
                });
            } catch (Exception e) {
                SwingUtilities.invokeLater(() -> {
                    logMessage("Error running tests: " + e.getMessage());
                    for (StackTraceElement element : e.getStackTrace()) {
                        logMessage("    " + element.toString());
                    }
                    progressBar.setString("Error running tests");
                    enableButtons();
                });
            }
        });
    }
    
    private boolean runTest(TestInfo test) {
        String testDir = baseDir + File.separator + "tests" + File.separator + 
                         test.language + File.separator + test.testName;
        
        File makeDir = new File(testDir + File.separator + "make");
        File bakeDir = new File(testDir + File.separator + "bake");
        
        if (!makeDir.exists() || !bakeDir.exists()) {
            logMessage("Error: Make or Bake directory not found for test " + test.testName);
            return false;
        }
        
        String logFilePath = logsDir + File.separator + test.language + "_" + test.testName + ".log";
        
        try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath))) {
            // Header information
            writer.println("Test: " + test.language + " - " + test.testName);
            writer.println("Date: " + dateFormat.format(new Date()));
            writer.println("========================================================");
            
            // Compare initial file state
            writer.println("\n=== Initial File Comparison ===");
            logMessage("Comparing initial files...");
            
            Map<String, FileInfo> makeFiles = scanDirectory(makeDir);
            Map<String, FileInfo> bakeFiles = scanDirectory(bakeDir);
            
            compareAndLogFiles(makeFiles, bakeFiles, writer);
            
            // Run make
            writer.println("\n=== Make Output ===");
            logMessage("Running make...");
            
            ProcessResult makeResult = runProcess("make", makeDir);
            writer.println(makeResult.output);
            logMessage(makeResult.output);
            
            // Run bake
            writer.println("\n=== Bake Output ===");
            logMessage("Running bake...");
            
            ProcessResult bakeResult = runProcess("java -cp bakefile.jar fr.monlouyan.bakefile.Main", bakeDir);
            writer.println(bakeResult.output);
            logMessage(bakeResult.output);
            
            // Compare results
            logMessage("Comparing results...");
            writer.println("\n=== Comparison Results ===");
            
            // Compare exit codes
            boolean exitCodesMatch = makeResult.exitCode == bakeResult.exitCode;
            writer.println("Exit codes match: " + exitCodesMatch);
            writer.println("Make exit code: " + makeResult.exitCode);
            writer.println("Bake exit code: " + bakeResult.exitCode);
            
            // Compare output patterns (ignoring the tool name differences)
            boolean outputPatternsMatch = compareOutputPatterns(makeResult.output, bakeResult.output);
            writer.println("Output patterns match: " + outputPatternsMatch);
            
            // Compare final file state
            writer.println("\n=== Final File State Comparison ===");
            
            Map<String, FileInfo> makeFinalFiles = scanDirectory(makeDir);
            Map<String, FileInfo> bakeFinalFiles = scanDirectory(bakeDir);
            
            compareAndLogFiles(makeFinalFiles, bakeFinalFiles, writer);
            
            // Check if files were created or modified as expected
            boolean fileChangesMatch = compareFileChanges(makeFiles, makeFinalFiles, bakeFiles, bakeFinalFiles);
            writer.println("File changes match: " + fileChangesMatch);
            
            // Test summary
            boolean testPassed = exitCodesMatch && outputPatternsMatch && fileChangesMatch;
            writer.println("\n=== Test Result ===");
            writer.println(testPassed ? "PASSED" : "FAILED");
            
            logMessage(testPassed ? "Test PASSED" : "Test FAILED");
            
            return testPassed;
            
        } catch (IOException e) {
            logMessage("Error running test: " + e.getMessage());
            return false;
        }
    }
    
    private boolean compareFileChanges(
            Map<String, FileInfo> makeInitial, 
            Map<String, FileInfo> makeFinal,
            Map<String, FileInfo> bakeInitial, 
            Map<String, FileInfo> bakeFinal) {
        
        // Check if the same files were created in both directories
        Set<String> makeCreated = new HashSet<>(makeFinal.keySet());
        makeCreated.removeAll(makeInitial.keySet());
        
        Set<String> bakeCreated = new HashSet<>(bakeFinal.keySet());
        bakeCreated.removeAll(bakeInitial.keySet());
        
        if (!makeCreated.equals(bakeCreated)) {
            logMessage("Different files created:\nMake: " + makeCreated + "\nBake: " + bakeCreated);
            return false;
        }
        
        // Check if the same files were modified
        boolean filesMatch = true;
        for (String file : makeInitial.keySet()) {
            if (makeFinal.containsKey(file) && bakeInitial.containsKey(file) && bakeFinal.containsKey(file)) {
                boolean makeModified = !makeInitial.get(file).equals(makeFinal.get(file));
                boolean bakeModified = !bakeInitial.get(file).equals(bakeFinal.get(file));
                
                if (makeModified != bakeModified) {
                    logMessage("File modification mismatch for " + file + 
                              "\nMake modified: " + makeModified + 
                              "\nBake modified: " + bakeModified);
                    filesMatch = false;
                }
            }
        }
        
        return filesMatch;
    }
    
    private ProcessResult runProcess(String command, File directory) throws IOException {
        ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", command);
        processBuilder.directory(directory);
        Process process = processBuilder.start();
        
        // Capture stdout and stderr
        StringBuilder output = new StringBuilder();
        try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
             BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
            
            String line;
            while ((line = stdInput.readLine()) != null) {
                output.append(line).append("\n");
            }
            
            while ((line = stdError.readLine()) != null) {
                output.append("ERROR: ").append(line).append("\n");
            }
        }
        
        try {
            process.waitFor();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        return new ProcessResult(process.exitValue(), output.toString());
    }
    
    private Map<String, FileInfo> scanDirectory(File directory) throws IOException {
        Map<String, FileInfo> files = new HashMap<>();
        
        if (!directory.exists() || !directory.isDirectory()) {
            return files;
        }
        
        Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                String relativePath = directory.toPath().relativize(file).toString();
                
                // Skip bakefile.jar to avoid differences
                if (relativePath.equals("bakefile.jar")) {
                    return FileVisitResult.CONTINUE;
                }
                
                FileInfo info = new FileInfo(
                    Files.isRegularFile(file),
                    attrs.size(),
                    attrs.lastModifiedTime().toMillis()
                );
                
                files.put(relativePath, info);
                return FileVisitResult.CONTINUE;
            }
        });
        
        return files;
    }
    
    private void compareAndLogFiles(Map<String, FileInfo> makeFiles, 
                                   Map<String, FileInfo> bakeFiles, 
                                   PrintWriter writer) {
        Set<String> allFiles = new HashSet<>();
        allFiles.addAll(makeFiles.keySet());
        allFiles.addAll(bakeFiles.keySet());
        
        List<String> sortedFiles = new ArrayList<>(allFiles);
        Collections.sort(sortedFiles);
        
        writer.println("File comparison:");
        for (String file : sortedFiles) {
            FileInfo makeInfo = makeFiles.get(file);
            FileInfo bakeInfo = bakeFiles.get(file);
            
            writer.print(file + ": ");
            if (makeInfo == null) {
                writer.println("Only in Bake");
            } else if (bakeInfo == null) {
                writer.println("Only in Make");
            } else if (makeInfo.equals(bakeInfo)) {
                writer.println("Identical");
            } else {
                writer.println("Different");
                writer.println("  Make: " + makeInfo);
                writer.println("  Bake: " + bakeInfo);
            }
        }
    }
    
    private boolean compareOutputPatterns(String makeOutput, String bakeOutput) {
        // Normalize output by replacing tool-specific words
        String normalizedMake = makeOutput.replaceAll("\\bmake\\b", "TOOL")
                                         .replaceAll("\\bMake\\b", "TOOL")
										 .replaceAll("ERROR: ", "");
        String normalizedBake = bakeOutput.replaceAll("\\bbake\\b", "TOOL")
                                         .replaceAll("\\bBake\\b", "TOOL")
										 .replaceAll("ERROR: ", "");;
        
        // Compare line by line, ignoring exact timestamps or specific paths
        String[] makeLines = normalizedMake.split("\n");
        String[] bakeLines = normalizedBake.split("\n");
        
        // If line counts are very different, they're probably not matching
        if (Math.abs(makeLines.length - bakeLines.length) > 2) {
            logMessage("Output line count mismatch: Make=" + makeLines.length + 
                      ", Bake=" + bakeLines.length);
            return false;
        }
        
        // Compare key patterns like error messages, file operations, etc.
        Pattern errorPattern = Pattern.compile(".*Error.*|.*\\*\\*\\*.*|.*failed.*", 
                                             Pattern.CASE_INSENSITIVE);
        Pattern commandPattern = Pattern.compile("^[a-z0-9_\\-]+ .*|^\\$.*");
        
        List<String> makeErrors = extractMatches(makeLines, errorPattern);
        List<String> bakeErrors = extractMatches(bakeLines, errorPattern);
        
        List<String> makeCommands = extractMatches(makeLines, commandPattern);
        List<String> bakeCommands = extractMatches(bakeLines, commandPattern);
        
        // If error counts are different, that's a significant difference
        if (makeErrors.size() != bakeErrors.size()) {
            logMessage("Error count mismatch: Make=" + makeErrors.size() + 
                      ", Bake=" + bakeErrors.size());
            return false;
        }
        
        // If command counts are different, that's a significant difference
        if (makeCommands.size() != bakeCommands.size()) {
            logMessage("Command count mismatch: Make=" + makeCommands.size() + 
                      ", Bake=" + bakeCommands.size());
            return false;
        }
        
        return true;
    }
    
    private List<String> extractMatches(String[] lines, Pattern pattern) {
        return Arrays.stream(lines)
                    .filter(line -> pattern.matcher(line).matches())
                    .collect(Collectors.toList());
    }
    
    private void logMessage(String message) {
        SwingUtilities.invokeLater(() -> {
            logArea.append(message + "\n");
            // Scroll to bottom
            logArea.setCaretPosition(logArea.getDocument().getLength());
        });
    }
    
    private static class TestInfo {
        int row;
        String language;
        String testName;
        
        TestInfo(int row, String language, String testName) {
            this.row = row;
            this.language = language;
            this.testName = testName;
        }
    }
    
    private static class FileInfo {
        boolean isFile;
        long size;
        long lastModified;
        
        FileInfo(boolean isFile, long size, long lastModified) {
            this.isFile = isFile;
            this.size = size;
            this.lastModified = lastModified;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof FileInfo)) {
                return false;
            }
            FileInfo other = (FileInfo) obj;
            return isFile == other.isFile && size == other.size;
            // We don't compare lastModified times directly
        }
        
        @Override
        public String toString() {
            return "isFile=" + isFile + ", size=" + size + ", lastModified=" + 
                   new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(lastModified));
        }
    }
    
    private static class ProcessResult {
        int exitCode;
        String output;
        
        ProcessResult(int exitCode, String output) {
            this.exitCode = exitCode;
            this.output = output;
        }
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            try {
                // Set native look and feel
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            BakeTestRunner runner = new BakeTestRunner();
            runner.setVisible(true);
        });
    }
}