package net.jsunit;

import net.jsunit.configuration.Configuration;
import net.jsunit.configuration.ServerType;
import net.jsunit.logging.BrowserResultRepository;
import net.jsunit.logging.FileBrowserResultRepository;
import net.jsunit.model.Browser;
import net.jsunit.model.BrowserResult;
import net.jsunit.utility.StringUtility;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JsUnitStandardServer extends AbstractJsUnitServer implements BrowserTestRunner {

    private List<BrowserResult> results = new ArrayList<BrowserResult>();
    private List<TestRunListener> browserTestRunListeners = new ArrayList<TestRunListener>();

    private ProcessStarter processStarter = new DefaultProcessStarter();
    private LaunchTestRunCommand launchTestRunCommand;
    private TimeoutChecker timeoutChecker;
    private Process browserProcess;
    private long timeLastResultReceived;

    private BrowserResultRepository browserResultRepository;

    public JsUnitStandardServer(Configuration configuration, boolean temporary) {
        this(configuration, new FileBrowserResultRepository(configuration.getLogsDirectory()), temporary);
    }

    public JsUnitStandardServer(Configuration configuration, BrowserResultRepository browserResultRepository, boolean temporary) {
        super(configuration, temporary ? ServerType.STANDARD_TEMPORARY : ServerType.STANDARD);
        this.browserResultRepository = browserResultRepository;
        addBrowserTestRunListener(new BrowserResultLogWriter(browserResultRepository));
        ServerRegistry.registerServer(this);
    }

    public static void main(String args[]) {
        try {
            JsUnitStandardServer server = new JsUnitStandardServer(Configuration.resolve(args), false);
            server.start();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    protected List<String> servletNames() {
        return Arrays.asList(new String[]{
                "acceptor",
                "config",
                "displayer",
                "index",
                "latestversion",
                "runner"
        });
    }

    public void accept(BrowserResult result) {
        long timeReceived = System.currentTimeMillis();
        if (launchTestRunCommand == null)
            return;
        Browser submittingBrowser = launchTestRunCommand.getBrowser();
        endBrowser();

        result.setBrowser(submittingBrowser);

        killTimeoutChecker();
        BrowserResult existingResultWithSameId = findResultWithId(result.getId(), submittingBrowser);
        for (TestRunListener listener : browserTestRunListeners)
            listener.browserTestRunFinished(submittingBrowser, result);
        if (existingResultWithSameId != null)
            results.remove(existingResultWithSameId);
        results.add(result);
        timeLastResultReceived = timeReceived;
    }

    private void killTimeoutChecker() {
        if (timeoutChecker != null) {
            timeoutChecker.die();
            timeoutChecker = null;
        }
    }

    public List<BrowserResult> getResults() {
        return results;
    }

    public void clearResults() {
        results.clear();
    }

    public BrowserResult findResultWithId(String id, int browserId) throws InvalidBrowserIdException {
        Browser browser = configuration.getBrowserById(browserId);
        if (browser == null)
            throw new InvalidBrowserIdException(browserId);
        return findResultWithId(id, browser);
    }

    private BrowserResult findResultWithId(String id, Browser browser) {
        BrowserResult result = findResultWithIdInResultList(id, browser);
        if (result == null)
            result = browserResultRepository.retrieve(id, browser);
        return result;
    }

    private BrowserResult findResultWithIdInResultList(String id, Browser browser) {
        for (BrowserResult result : getResults()) {
            if (result.hasId(id) && result.isForBrowser(browser))
                return result;
        }
        return null;
    }

    public BrowserResult lastResult() {
        List results = getResults();
        return results.isEmpty()
                ? null
                : (BrowserResult) results.get(results.size() - 1);
    }

    public int resultsCount() {
        return getResults().size();
    }

    public String toString() {
        return "JsUnit Server";
    }

    public List<Browser> getBrowsers() {
        return configuration.getBrowsers();
    }

    public boolean hasReceivedResultSince(long launchTime) {
        return timeLastResultReceived >= launchTime;
    }

    public void addBrowserTestRunListener(TestRunListener listener) {
        browserTestRunListeners.add(listener);
    }

    public List<TestRunListener> getBrowserTestRunListeners() {
        return browserTestRunListeners;
    }

    private void endBrowser() {
        if (browserProcess != null && configuration.shouldCloseBrowsersAfterTestRuns()) {
            if (launchTestRunCommand.getBrowserKillCommand() != null) {
                try {
                    processStarter.execute(new String[]{launchTestRunCommand.getBrowserKillCommand()});
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                browserProcess.destroy();
                try {
                    browserProcess.waitFor();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                waitUntilProcessHasExitValue(browserProcess);
            }
        }
        browserProcess = null;
        launchTestRunCommand = null;
        killTimeoutChecker();
    }

    private void waitUntilProcessHasExitValue(Process browserProcess) {
        while (true) {
            try {
                if (browserProcess != null)
                    browserProcess.exitValue();
                return;
            } catch (IllegalThreadStateException e) {
            }
        }
    }

    public long launchBrowserTestRun(BrowserLaunchSpecification launchSpec) {
        waitUntilLastReceivedTimeHasPassed();
        long launchTime = System.currentTimeMillis();
        launchTestRunCommand = new LaunchTestRunCommand(launchSpec, configuration);
        Browser browser = launchTestRunCommand.getBrowser();
        String browserFileName = browser.getFileName();
        try {
            logStatus("Launching " + browserFileName + " on " + launchTestRunCommand.getTestURL());
            for (TestRunListener listener : browserTestRunListeners)
                listener.browserTestRunStarted(browser);
            this.browserProcess = processStarter.execute(launchTestRunCommand.generateArray());
            startTimeoutChecker(launchTime);
        } catch (Throwable throwable) {
            handleCrashWhileLaunching(throwable);
        }
        return launchTime;
    }

    private void handleCrashWhileLaunching(Throwable throwable) {
        Browser browser = launchTestRunCommand.getBrowser();
        logStatus("Browser " + browser.getFileName() + " failed to launch: " + StringUtility.stackTraceAsString(throwable));
        BrowserResult failedToLaunchBrowserResult = new BrowserResult();
        failedToLaunchBrowserResult.setFailedToLaunch();
        failedToLaunchBrowserResult.setBrowser(browser);
        failedToLaunchBrowserResult.setServerSideException(throwable);
        accept(failedToLaunchBrowserResult);
    }

    private void waitUntilLastReceivedTimeHasPassed() {
        while (System.currentTimeMillis() == timeLastResultReceived)
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
    }

    private void startTimeoutChecker(long launchTime) {
        timeoutChecker = new TimeoutChecker(browserProcess, launchTestRunCommand.getBrowser(), launchTime, this);
        timeoutChecker.start();
    }

    void setProcessStarter(ProcessStarter starter) {
        this.processStarter = starter;
    }

    public void startTestRun() {
        for (TestRunListener listener : browserTestRunListeners) {
            listener.testRunStarted();
            while (!listener.isReady())
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                }
        }
    }

    public void finishTestRun() {
        for (TestRunListener listener : browserTestRunListeners)
            listener.testRunFinished();
        testRunCount ++;
    }

    public Process getBrowserProcess() {
        return browserProcess;
    }

    public void dispose() {
        super.dispose();
        endBrowser();
    }

    protected String xworkXmlName() {
        return "xwork.xml";
    }

    public int timeoutSeconds() {
        return configuration.getTimeoutSeconds();
    }

    public boolean isAwaitingBrowserSubmission() {
        return launchTestRunCommand != null;
    }
}
