package junit.swingui;

/*-
 * #%L
 * org.eclipse.jdt.ui.junit.sampleproject
 * %%
 * Copyright (C) 2020 Eclipse Foundation
 * %%
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 * #L%
 */

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.URL;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;
import junit.framework.*;
import junit.runner.*;

/**
 * A Swing based user interface to run tests. Enter the name of a class which
 * either provides a static suite method or is a subclass of TestCase.
 * 
 * <pre>
 * Synopsis: java junit.swingui.TestRunner [-noloading] [TestCase]
 * </pre>
 * 
 * TestRunner takes as an optional argument the name of the testcase class to be
 * run.
 */
public class TestRunner extends BaseTestRunner implements TestRunContext {
	private static final int GAP = 4;
	private static final int HISTORY_LENGTH = 5;

	protected JFrame fFrame;
	private Thread fRunner;
	private TestResult fTestResult;

	private JComboBox fSuiteCombo;
	private ProgressBar fProgressIndicator;
	private DefaultListModel fFailures;
	private JLabel fLogo;
	private CounterPanel fCounterPanel;
	private JButton fRun;
	private JButton fQuitButton;
	private JButton fRerunButton;
	private StatusLine fStatusLine;
	private FailureDetailView fFailureView;
	private JTabbedPane fTestViewTab;
	private JCheckBox fUseLoadingRunner;
	private Vector fTestRunViews = new Vector(); // view associated with tab in tabbed pane

	private static final String TESTCOLLECTOR_KEY = "TestCollectorClass";
	private static final String FAILUREDETAILVIEW_KEY = "FailureViewClass";

	public TestRunner() {
	}

	public static void main(String[] args) {
		new TestRunner().start(args);
	}

	public static void run(Class test) {
		String args[] = { test.getName() };
		main(args);
	}

	public void testFailed(final int status, final Test test, final Throwable t) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				switch (status) {
				case TestRunListener.STATUS_ERROR:
					fCounterPanel.setErrorValue(fTestResult.errorCount());
					appendFailure(test, t);
					break;
				case TestRunListener.STATUS_FAILURE:
					fCounterPanel.setFailureValue(fTestResult.failureCount());
					appendFailure(test, t);
					break;
				}
			}
		});
	}

	public void testStarted(String testName) {
		postInfo("Running: " + testName);
	}

	public void testEnded(String stringName) {
		synchUI();
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				if (fTestResult != null) {
					fCounterPanel.setRunValue(fTestResult.runCount());
					fProgressIndicator.step(fTestResult.runCount(), fTestResult.wasSuccessful());
				}
			}
		});
	}

	public void setSuite(String suiteName) {
		fSuiteCombo.getEditor().setItem(suiteName);
	}

	private void addToHistory(final String suite) {
		for (int i = 0; i < fSuiteCombo.getItemCount(); i++) {
			if (suite.equals(fSuiteCombo.getItemAt(i))) {
				fSuiteCombo.removeItemAt(i);
				fSuiteCombo.insertItemAt(suite, 0);
				fSuiteCombo.setSelectedIndex(0);
				return;
			}
		}
		fSuiteCombo.insertItemAt(suite, 0);
		fSuiteCombo.setSelectedIndex(0);
		pruneHistory();
	}

	private void pruneHistory() {
		int historyLength = getPreference("maxhistory", HISTORY_LENGTH);
		if (historyLength < 1)
			historyLength = 1;
		for (int i = fSuiteCombo.getItemCount() - 1; i > historyLength - 1; i--)
			fSuiteCombo.removeItemAt(i);
	}

	private void appendFailure(Test test, Throwable t) {
		fFailures.addElement(new TestFailure(test, t));
		if (fFailures.size() == 1)
			revealFailure(test);
	}

	private void revealFailure(Test test) {
		for (Object fTestRunView : fTestRunViews) {
			TestRunView v = (TestRunView) fTestRunView;
			v.revealFailure(test);
		}
	}

	protected void aboutToStart(final Test testSuite) {
		for (Object fTestRunView : fTestRunViews) {
			TestRunView v = (TestRunView) fTestRunView;
			v.aboutToStart(testSuite, fTestResult);
		}
	}

	protected void runFinished(final Test testSuite) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				for (Object fTestRunView : fTestRunViews) {
					TestRunView v = (TestRunView) fTestRunView;
					v.runFinished(testSuite, fTestResult);
				}
			}
		});
	}

	protected CounterPanel createCounterPanel() {
		return new CounterPanel();
	}

	protected JPanel createFailedPanel() {
		JPanel failedPanel = new JPanel(new GridLayout(0, 1, 0, 2));
		fRerunButton = new JButton("Run");
		fRerunButton.setEnabled(false);
		fRerunButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				rerun();
			}
		});
		failedPanel.add(fRerunButton);
		return failedPanel;
	}

	protected FailureDetailView createFailureDetailView() {
		String className = BaseTestRunner.getPreference(FAILUREDETAILVIEW_KEY);
		if (className != null) {
			Class viewClass = null;
			try {
				viewClass = Class.forName(className);
				return (FailureDetailView) viewClass.newInstance();
			} catch (Exception e) {
				JOptionPane.showMessageDialog(fFrame, "Could not create Failure DetailView - using default view");
			}
		}
		return new DefaultFailureDetailView();
	}

	/**
	 * Creates the JUnit menu. Clients override this method to add additional menu
	 * items.
	 */
	protected JMenu createJUnitMenu() {
		JMenu menu = new JMenu("JUnit");
		menu.setMnemonic('J');
		JMenuItem mi1 = new JMenuItem("About...");
		mi1.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				about();
			}
		});
		mi1.setMnemonic('A');
		menu.add(mi1);

		menu.addSeparator();
		JMenuItem mi2 = new JMenuItem(" Exit ");
		mi2.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				terminate();
			}
		});
		mi2.setMnemonic('x');
		menu.add(mi2);

		return menu;
	}

	protected JFrame createFrame() {
		JFrame frame = new JFrame("JUnit");
		Image icon = loadFrameIcon();
		if (icon != null)
			frame.setIconImage(icon);
		frame.getContentPane().setLayout(new BorderLayout(0, 0));

		frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				terminate();
			}
		});
		return frame;
	}

	protected JLabel createLogo() {
		JLabel label;
		Icon icon = getIconResource(BaseTestRunner.class, "logo.gif");
		if (icon != null)
			label = new JLabel(icon);
		else
			label = new JLabel("JV");
		label.setToolTipText("JUnit Version " + Version.id());
		return label;
	}

	protected void createMenus(JMenuBar mb) {
		mb.add(createJUnitMenu());
	}

	protected JCheckBox createUseLoaderCheckBox() {
		boolean useLoader = useReloadingTestSuiteLoader();
		JCheckBox box = new JCheckBox("Reload classes every run", useLoader);
		box.setToolTipText("Use a custom class loader to reload the classes for every run");
		if (inVAJava())
			box.setVisible(false);
		return box;
	}

	protected JButton createQuitButton() {
		// spaces required to avoid layout flicker
		// Exit is shorter than Stop that shows in the same column
		JButton quit = new JButton(" Exit ");
		quit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				terminate();
			}
		});
		return quit;
	}

	protected JButton createRunButton() {
		JButton run = new JButton("Run");
		run.setEnabled(true);
		run.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				runSuite();
			}
		});
		return run;
	}

	protected Component createBrowseButton() {
		JButton browse = new JButton("...");
		browse.setToolTipText("Select a Test class");
		browse.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				browseTestClasses();
			}
		});
		return browse;
	}

	protected StatusLine createStatusLine() {
		return new StatusLine(380);
	}

	protected JComboBox createSuiteCombo() {
		JComboBox combo = new JComboBox();
		combo.setEditable(true);
		combo.setLightWeightPopupEnabled(false);

		combo.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent e) {
				textChanged();
				if (e.getKeyChar() == KeyEvent.VK_ENTER)
					runSuite();
			}
		});
		try {
			loadHistory(combo);
		} catch (IOException e) {
			// fails the first time
		}
		combo.addItemListener(new ItemListener() {
			public void itemStateChanged(ItemEvent event) {
				if (event.getStateChange() == ItemEvent.SELECTED) {
					textChanged();
				}
			}
		});
		return combo;
	}

	protected JTabbedPane createTestRunViews() {
		JTabbedPane pane = new JTabbedPane(JTabbedPane.BOTTOM);

		FailureRunView lv = new FailureRunView(this);
		fTestRunViews.addElement(lv);
		lv.addTab(pane);

		TestHierarchyRunView tv = new TestHierarchyRunView(this);
		fTestRunViews.addElement(tv);
		tv.addTab(pane);

		pane.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				testViewChanged();
			}
		});
		return pane;
	}

	public void testViewChanged() {
		TestRunView view = (TestRunView) fTestRunViews.elementAt(fTestViewTab.getSelectedIndex());
		view.activate();
	}

	protected TestResult createTestResult() {
		return new TestResult();
	}

	protected JFrame createUI(String suiteName) {
		JFrame frame = createFrame();
		JMenuBar mb = new JMenuBar();
		createMenus(mb);
		frame.setJMenuBar(mb);

		JLabel suiteLabel = new JLabel("Test class name:");
		fSuiteCombo = createSuiteCombo();
		fRun = createRunButton();
		frame.getRootPane().setDefaultButton(fRun);
		Component browseButton = createBrowseButton();

		fUseLoadingRunner = createUseLoaderCheckBox();
		fProgressIndicator = new ProgressBar();
		fCounterPanel = createCounterPanel();

		fFailures = new DefaultListModel();

		fTestViewTab = createTestRunViews();
		JPanel failedPanel = createFailedPanel();

		fFailureView = createFailureDetailView();
		JScrollPane tracePane = new JScrollPane(fFailureView.getComponent(), JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
				JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

		fStatusLine = createStatusLine();
		fQuitButton = createQuitButton();
		fLogo = createLogo();

		JPanel panel = new JPanel(new GridBagLayout());

		addGrid(panel, suiteLabel, 0, 0, 2, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.WEST);
		addGrid(panel, fSuiteCombo, 0, 1, 1, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.WEST);
		addGrid(panel, browseButton, 1, 1, 1, GridBagConstraints.NONE, 0.0, GridBagConstraints.WEST);
		addGrid(panel, fRun, 2, 1, 1, GridBagConstraints.HORIZONTAL, 0.0, GridBagConstraints.CENTER);

		addGrid(panel, fUseLoadingRunner, 0, 2, 3, GridBagConstraints.NONE, 1.0, GridBagConstraints.WEST);
		// addGrid(panel, new JSeparator(), 0, 3, 3, GridBagConstraints.HORIZONTAL, 1.0,
		// GridBagConstraints.WEST);

		addGrid(panel, fProgressIndicator, 0, 3, 2, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.WEST);
		addGrid(panel, fLogo, 2, 3, 1, GridBagConstraints.NONE, 0.0, GridBagConstraints.NORTH);

		addGrid(panel, fCounterPanel, 0, 4, 2, GridBagConstraints.NONE, 0.0, GridBagConstraints.WEST);
		addGrid(panel, new JSeparator(), 0, 5, 2, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.WEST);
		addGrid(panel, new JLabel("Results:"), 0, 6, 2, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.WEST);

		JSplitPane splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT, fTestViewTab, tracePane);
		addGrid(panel, splitter, 0, 7, 2, GridBagConstraints.BOTH, 1.0, GridBagConstraints.WEST);

		addGrid(panel, failedPanel, 2, 7, 1, GridBagConstraints.HORIZONTAL, 0.0, GridBagConstraints.NORTH/* CENTER */);

		addGrid(panel, fStatusLine, 0, 9, 2, GridBagConstraints.HORIZONTAL, 1.0, GridBagConstraints.CENTER);
		addGrid(panel, fQuitButton, 2, 9, 1, GridBagConstraints.HORIZONTAL, 0.0, GridBagConstraints.CENTER);

		frame.setContentPane(panel);
		frame.pack();
		frame.setLocation(200, 200);
		return frame;
	}

	private void addGrid(JPanel p, Component co, int x, int y, int w, int fill, double wx, int anchor) {
		GridBagConstraints c = new GridBagConstraints();
		c.gridx = x;
		c.gridy = y;
		c.gridwidth = w;
		c.anchor = anchor;
		c.weightx = wx;
		c.fill = fill;
		if (fill == GridBagConstraints.BOTH || fill == GridBagConstraints.VERTICAL)
			c.weighty = 1.0;
		c.insets = new Insets(y == 0 ? 10 : 0, x == 0 ? 10 : GAP, GAP, GAP);
		p.add(co, c);
	}

	protected String getSuiteText() {
		if (fSuiteCombo == null)
			return "";
		return (String) fSuiteCombo.getEditor().getItem();
	}

	public ListModel getFailures() {
		return fFailures;
	}

	public void insertUpdate(DocumentEvent event) {
		textChanged();
	}

	public void browseTestClasses() {
		TestCollector collector = createTestCollector();
		TestSelector selector = new TestSelector(fFrame, collector);
		if (selector.isEmpty()) {
			JOptionPane.showMessageDialog(fFrame,
					"No Test Cases found.\nCheck that the configured \'TestCollector\' is supported on this platform.");
			return;
		}
		selector.show();
		String className = selector.getSelectedItem();
		if (className != null)
			setSuite(className);
	}

	TestCollector createTestCollector() {
		String className = BaseTestRunner.getPreference(TESTCOLLECTOR_KEY);
		if (className != null) {
			Class collectorClass = null;
			try {
				collectorClass = Class.forName(className);
				return (TestCollector) collectorClass.newInstance();
			} catch (Exception e) {
				JOptionPane.showMessageDialog(fFrame, "Could not create TestCollector - using default collector");
			}
		}
		return new SimpleTestCollector();
	}

	private Image loadFrameIcon() {
		ImageIcon icon = (ImageIcon) getIconResource(BaseTestRunner.class, "smalllogo.gif");
		if (icon != null)
			return icon.getImage();
		return null;
	}

	private void loadHistory(JComboBox combo) throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(getSettingsFile()));
		int itemCount = 0;
		try {
			String line;
			while ((line = br.readLine()) != null) {
				combo.addItem(line);
				itemCount++;
			}
			if (itemCount > 0)
				combo.setSelectedIndex(0);

		} finally {
			br.close();
		}
	}

	private File getSettingsFile() {
		String home = System.getProperty("user.home");
		return new File(home, ".junitsession");
	}

	private void postInfo(final String message) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				showInfo(message);
			}
		});
	}

	private void postStatus(final String status) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				showStatus(status);
			}
		});
	}

	public void removeUpdate(DocumentEvent event) {
		textChanged();
	}

	private void rerun() {
		TestRunView view = (TestRunView) fTestRunViews.elementAt(fTestViewTab.getSelectedIndex());
		Test rerunTest = view.getSelectedTest();
		if (rerunTest != null)
			rerunTest(rerunTest);
	}

	private void rerunTest(Test test) {
		if (!(test instanceof TestCase)) {
			showInfo("Could not reload " + test.toString());
			return;
		}
		Test reloadedTest = null;
		TestCase rerunTest = (TestCase) test;

		try {
			Class reloadedTestClass = getLoader().reload(test.getClass());
			reloadedTest = TestSuite.createTest(reloadedTestClass, rerunTest.getName());
		} catch (Exception e) {
			showInfo("Could not reload " + test.toString());
			return;
		}
		TestResult result = new TestResult();
		reloadedTest.run(result);

		String message = reloadedTest.toString();
		if (result.wasSuccessful())
			showInfo(message + " was successful");
		else if (result.errorCount() == 1)
			showStatus(message + " had an error");
		else
			showStatus(message + " had a failure");
	}

	protected void reset() {
		fCounterPanel.reset();
		fProgressIndicator.reset();
		fRerunButton.setEnabled(false);
		fFailureView.clear();
		fFailures.clear();
	}

	protected void runFailed(String message) {
		showStatus(message);
		fRun.setText("Run");
		fRunner = null;
	}

	synchronized public void runSuite() {
		if (fRunner != null) {
			fTestResult.stop();
		} else {
			setLoading(shouldReload());
			reset();
			showInfo("Load Test Case...");
			final String suiteName = getSuiteText();
			final Test testSuite = getTest(suiteName);
			if (testSuite != null) {
				addToHistory(suiteName);
				doRunTest(testSuite);
			}
		}
	}

	private boolean shouldReload() {
		return !inVAJava() && fUseLoadingRunner.isSelected();
	}

	synchronized protected void runTest(final Test testSuite) {
		if (fRunner != null) {
			fTestResult.stop();
		} else {
			reset();
			if (testSuite != null) {
				doRunTest(testSuite);
			}
		}
	}

	private void doRunTest(final Test testSuite) {
		setButtonLabel(fRun, "Stop");
		fRunner = new Thread("TestRunner-Thread") {
			public void run() {
				TestRunner.this.start(testSuite);
				postInfo("Running...");

				long startTime = System.currentTimeMillis();
				testSuite.run(fTestResult);

				if (fTestResult.shouldStop()) {
					postStatus("Stopped");
				} else {
					long endTime = System.currentTimeMillis();
					long runTime = endTime - startTime;
					postInfo("Finished: " + elapsedTimeAsString(runTime) + " seconds");
				}
				runFinished(testSuite);
				setButtonLabel(fRun, "Run");
				fRunner = null;
				System.gc();
			}
		};
		// make sure that the test result is created before we start the
		// test runner thread so that listeners can register for it.
		fTestResult = createTestResult();
		fTestResult.addListener(TestRunner.this);
		aboutToStart(testSuite);

		fRunner.start();
	}

	private void saveHistory() throws IOException {
		BufferedWriter bw = new BufferedWriter(new FileWriter(getSettingsFile()));
		try {
			for (int i = 0; i < fSuiteCombo.getItemCount(); i++) {
				String testsuite = fSuiteCombo.getItemAt(i).toString();
				bw.write(testsuite, 0, testsuite.length());
				bw.newLine();
			}
		} finally {
			bw.close();
		}
	}

	private void setButtonLabel(final JButton button, final String label) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				button.setText(label);
			}
		});
	}

	public void handleTestSelected(Test test) {
		fRerunButton.setEnabled(test != null && (test instanceof TestCase));
		showFailureDetail(test);
	}

	private void showFailureDetail(Test test) {
		if (test != null) {
			ListModel failures = getFailures();
			for (int i = 0; i < failures.getSize(); i++) {
				TestFailure failure = (TestFailure) failures.getElementAt(i);
				if (failure.failedTest() == test) {
					fFailureView.showFailure(failure);
					return;
				}
			}
		}
		fFailureView.clear();
	}

	private void showInfo(String message) {
		fStatusLine.showInfo(message);
	}

	private void showStatus(String status) {
		fStatusLine.showError(status);
	}

	/**
	 * Starts the TestRunner
	 */
	public void start(String[] args) {
		String suiteName = processArguments(args);
		fFrame = createUI(suiteName);
		fFrame.pack();
		fFrame.setVisible(true);

		if (suiteName != null) {
			setSuite(suiteName);
			runSuite();
		}
	}

	private void start(final Test test) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				int total = test.countTestCases();
				fProgressIndicator.start(total);
				fCounterPanel.setTotal(total);
			}
		});
	}

	/**
	 * Wait until all the events are processed in the event thread
	 */
	private void synchUI() {
		try {
			SwingUtilities.invokeAndWait(new Runnable() {
				public void run() {
				}
			});
		} catch (Exception e) {
		}
	}

	/**
	 * Terminates the TestRunner
	 */
	public void terminate() {
		fFrame.dispose();
		try {
			saveHistory();
		} catch (IOException e) {
			System.out.println("Couldn't save test run history");
		}
		System.exit(0);
	}

	public void textChanged() {
		fRun.setEnabled(getSuiteText().length() > 0);
		clearStatus();
	}

	protected void clearStatus() {
		fStatusLine.clear();
	}

	public static Icon getIconResource(Class clazz, String name) {
		URL url = clazz.getResource(name);
		if (url == null) {
			System.err.println("Warning: could not load \"" + name + "\" icon");
			return null;
		}
		return new ImageIcon(url);
	}

	private void about() {
		AboutDialog about = new AboutDialog(fFrame);
		about.show();
	}
}
