package be.iminds.ilabt.jfed.ui.javafx.automated_testing_gui;

import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsFactory;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.history.ApiCallHistory;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api.test.TestClassList;
import be.iminds.ilabt.jfed.preferences.GuiPreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.testing.base.ApiTest;
import be.iminds.ilabt.jfed.testing.base.ApiTestFactory;
import be.iminds.ilabt.jfed.testing.base.ApiTestResult;
import be.iminds.ilabt.jfed.testing.base.AutomatedTestRunner;
import be.iminds.ilabt.jfed.ui.javafx.choosers.ServerChooser;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.ui.javafx.log_gui.LogHistoryPanel;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments.CommandArgumentChooser;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments.MultiLineStringArgumentChooser;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments.StringArgumentChooser;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.JFedUtils;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ProbeController
 */
public class AutomatedTester {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AutomatedTester.class);
    Task<ApiTestResult> runTask;

    @FXML
    private ServerChooser targetAuthChooserController;
    @FXML
    private Label userAuthLabel;
    @FXML
    private ListView<String> testClassesListView;
    @FXML
    private ListView<String> testGroupsListView;
    @FXML
    private Button runButton;
    @FXML
    private ScrollPane argumentsScrollPane;
    @FXML
    private VBox argumentsBox;
    @FXML
    private WebView webView;
    @FXML
    private Parent progressBarBox;
    @FXML
    private Label progressLabel;
    @FXML
    private ProgressBar progressBar;
    private ObservableList<String> testGroups = FXCollections.observableArrayList();
    private Class<? extends ApiTest> testClass;
    private Map<String, CommandArgumentChooser> extraArguments = new HashMap<>();
    private Stage historyStage;

    private final GeniUserProvider geniUserProvider;
    private final ApiCallHistory apiCallHistory;
    private final TestbedInfoSource testbedInfoSource;
    private final Logger logger;
    private final Provider<LogHistoryPanel> logHistoryPanelProvider;
    private final JFedGuiPreferences jFedPreferences;
    private final ApiTestFactory apiTestFactory;
    private final SerializableApiCallDetailsFactory serializableApiCallDetailsFactory;

    @Inject
    public AutomatedTester(GeniUserProvider geniUserProvider, ApiCallHistory apiCallHistory,
                           TestbedInfoSource testbedInfoSource, Logger logger, Provider<LogHistoryPanel> logHistoryPanelProvider,
                           JFedGuiPreferences jFedPreferences,
                           ApiTestFactory apiTestFactory,
                           SerializableApiCallDetailsFactory serializableApiCallDetailsFactory) {
        this.geniUserProvider = geniUserProvider;
        this.apiCallHistory = apiCallHistory;
        this.testbedInfoSource = testbedInfoSource;
        this.logger = logger;
        this.logHistoryPanelProvider = logHistoryPanelProvider;
        this.jFedPreferences = jFedPreferences;
        this.apiTestFactory = apiTestFactory;
        this.serializableApiCallDetailsFactory = serializableApiCallDetailsFactory;
    }

    @FXML
    private void initialize() {
        ObservableList<String> testClasses = FXCollections.observableArrayList(allUnitTestClasses());
        testClassesListView.setItems(testClasses);
        testGroupsListView.setItems(testGroups);

        testClassesListView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldSelectedItem, newSelectedItem) -> updateSelectedClass(newSelectedItem));
        updateSelectedClass(testClassesListView.getSelectionModel().selectedItemProperty().get());

        progressBarBox.managedProperty().bind(progressBarBox.visibleProperty());
        progressBarBox.setVisible(false);
    }


    public static List<String> allUnitTestClasses() {
        return TestClassList.getInstance().allTestClasses();
    }

    public void updateSelectedClass(String selectedClassName) {
        if (selectedClassName == null) {
            testClass = null;
            testGroups.clear();
            return;
        }

        testClass = null;
        try {
            testClass = Class.forName(selectedClassName).asSubclass(ApiTest.class);
            assert ApiTest.class.isAssignableFrom(testClass) : testClass.getName() + " is not a ApiTest class";
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Bug in Test GUI: could not find test class " + selectedClassName, e);
        }
        assert testClass != null;

        testGroups.setAll(testGroups(testClass));

        List<String> requiredProps = new ArrayList<>();
        List<String> optionalProps = new ArrayList<>();

        optionalProps = ApiTest.getOptionalConfigKeys(testClass);
        requiredProps = ApiTest.getRequiredConfigKeys(testClass);

        extraArguments.clear();
        argumentsBox.getChildren().clear();

        List<String> allKeys = new ArrayList<>(requiredProps);
        allKeys.addAll(optionalProps);
        if (!allKeys.isEmpty()) {
            Label head = new Label("Extra Test Arguments: ");
            VBox.setMargin(head, new Insets(10.0/*top*/, 0.0/*right*/, 0.0/*bottom*/, 0.0/*left*/));
            argumentsBox.getChildren().add(head);

            for (String key : allKeys) {
                boolean required = requiredProps.contains(key);

                CommandArgumentChooser commandArgumentChooser_helper = null;
                if (key.equals("fixed_rspec") || key.trim().toLowerCase().endsWith("credential")) {
                    commandArgumentChooser_helper = new MultiLineStringArgumentChooser("", true);
                } else
                    commandArgumentChooser_helper = new StringArgumentChooser("");
                final CommandArgumentChooser commandArgumentChooser = commandArgumentChooser_helper;
                HBox hBox = new HBox();
                Label l = new Label(key + ":");
                HBox.setMargin(l, new Insets(0.0/*top*/, 10.0/*right*/, 0.0/*bottom*/, 0.0/*left*/));
                HBox.setHgrow(commandArgumentChooser, Priority.ALWAYS);
                hBox.getChildren().addAll(l, commandArgumentChooser);
                VBox.setMargin(hBox, new Insets(10.0/*top*/, 0.0/*right*/, 0.0/*bottom*/, 10.0/*left*/));

                if (key.endsWith("_file")) {
                    Button b = new Button("Choose File...");
                    hBox.getChildren().add(b);
                    b.setOnAction(actionEvent -> {
                        FileChooser fc = new FileChooser();

                        //set the default open dir
                        if (System.getProperty("user.home") != null)
                            fc.setInitialDirectory(new File(System.getProperty("user.home")));

                        if (jFedPreferences.containsPreference(GuiPreferenceKey.PREF_RSPECFOLDER_REQUEST)) {
                            File dir =
                                    new File(jFedPreferences.getString(GuiPreferenceKey.PREF_RSPECFOLDER_REQUEST));
                            if (dir.exists()) {
                                fc.setInitialDirectory(dir);
                            }
                        }

                        File file = fc.showOpenDialog(null);

                        if (file == null)
                            return;

                        ((StringArgumentChooser) commandArgumentChooser).setValue(file.getAbsolutePath());
                    });
                }

                argumentsBox.getChildren().add(hBox);
                extraArguments.put(key, commandArgumentChooser);
            }
        }

        if (runTask == null)
            progressBarBox.setVisible(false);
    }

    public List<String> testGroups(Class<? extends ApiTest> testClass) {
        List<String> res = Arrays.stream(testClass.getMethods())
                .flatMap(m -> Arrays.stream(m.getDeclaredAnnotations()))
                .filter(annotation -> annotation instanceof ApiTest.Test)
                .map(ApiTest.Test.class::cast)
                .flatMap(testAnnotation -> Arrays.stream(testAnnotation.groups()))
                .distinct()
                .collect(Collectors.toList());

        res.add("<no group>");
        return res;
    }

    @FXML
    private void runTests() {
        if (runTask == null)
            progressBarBox.setVisible(false);

        if (runTask != null) {
            JFDialogs.create()
                    .owner(runButton.getScene().getWindow())
                    .message("Already running tests. Wait until current tests are finished.")
                    .showError();
            return;
        }
        if (!geniUserProvider.isUserLoggedIn()) {
            JFDialogs.create()
                    .owner(runButton.getScene().getWindow())
                    .message("Can only run tests if user is logged in: log in first")
                    .showError();
            return;
        }
        if (targetAuthChooserController.getSelectedServer() == null) {
            JFDialogs.create()
                    .owner(runButton.getScene().getWindow())
                    .message("No target authority is selected")
                    .showError();
            return;
        }
        if (testClassesListView.getSelectionModel().selectedItemProperty() == null || testClass == null) {
            JFDialogs.create()
                    .owner(runButton.getScene().getWindow())
                    .message("No test is selected")
                    .showError();
            return;
        }
        assert testClass != null;

        GeniUser user = geniUserProvider.getLoggedInGeniUser();

        Server authForRspec = targetAuthChooserController.getSelectedServer();

        //do not auto select needed auth like this:
//        Server authToConnect = authorityFinder.findByUrn(
//                targetAuthChooserController.getSelectedServer().getUrn(),
//                AuthorityFinder.Purpose.CREATE_SLIVER);
        //bacuase user chooses auth from list, so we will contact only explicitly requested auth
        Server authToConnect = targetAuthChooserController.getSelectedServer();

        TargetAuthority testedAuthority = new TargetAuthority(targetAuthChooserController.getSelectedServer().getDefaultComponentManagerAsGeniUrn(), authForRspec, authToConnect);

        String group = testGroupsListView.getSelectionModel().getSelectedItem();
        if (Objects.equals(group, "<no group>"))
            group = null;
        final String testGroup = group;

        final Properties props = new Properties();
        for (Map.Entry<String, CommandArgumentChooser> e : extraArguments.entrySet()) {
            String key = e.getKey();
            CommandArgumentChooser chooser = e.getValue();
            if (chooser.valueProperty() != null && !Objects.equals(((String) chooser.valueProperty().getValue()).trim(), ""))
                props.setProperty(key, (String) chooser.valueProperty().getValue());
        }

        LOG.debug("Running Test: {} group={}", testClass.getName(), group);

        runTask = new Task<ApiTestResult>() {
            @Override
            protected ApiTestResult call() throws Exception {
                LOG.debug("   Starting Tests");
                updateMessage("Starting Tests");
                updateProgress(0, 100);

                try {
                    Platform.runLater(() -> {
                        shownResult = null;
                        shownHtmlOutput = null;
                        WebEngine webEngine = webView.getEngine();
                        webEngine.loadContent("<html><body><p>Running test...</p></body></html>");
                    });

                    ApiTest test = apiTestFactory.createApiTest(logger, testedAuthority, testClass);

                    AutomatedTestRunner automatedTestRunner = new AutomatedTestRunner(logger, testedAuthority, user, serializableApiCallDetailsFactory);
                    shownResult =
                            automatedTestRunner.runTest(props, test, testGroup, new AutomatedTestRunner.TestListener() {
                                @Override
                                public void onStart(String testname, int testNr, int testCount) {
                                    LOG.debug("Running test: {} ...", testname);
                                    updateMessage("Running test: " + testname);
                                    updateProgress(testNr - 1, testCount);
                                }

                                @Override
                                public void onResult(ApiTestResult.ApiTestMethodResult result, int testNr, int testCount) {
                                    LOG.debug("     Test result: {}", result);
                                    updateProgress(testNr, testCount);
                                }

                                @Override
                                public void onAllTestDone(ApiTestResult result, int testCount) {
                                    updateProgress(testCount, testCount);
                                    updateMessage("All tests completed");
                                }
                            }, false);

                    updateMessage("Test Finished. Generating HTML report...");
                    LOG.debug("Test Finished. Generating HTML report...");

                    try {
                        shownHtmlOutput = shownResult.toHtmlString(true/*showSecurityRisk*/);
                    } catch (AssertionError | Exception e) {
                        LOG.error("Error while generating HTML: ", e);
                        shownHtmlOutput = "Error while generating HTML: "+e.getMessage();
                    }
                    updateMessage("Test Finished. HTML report created.");
                    LOG.debug("Test Finished. HTML report created.");

                    Platform.runLater(() -> {
                        WebEngine webEngine = webView.getEngine();
                        webEngine.loadContent(shownHtmlOutput);
                        LOG.debug("   Showing test");
                    });

                    runTask = null;
                    return shownResult;
                } catch (AssertionError e) {
                    LOG.error("Something went wrong while running tests: " + e, e);
                    updateMessage("AssertionError has occured while running test.");
                    return null;
                } catch (Exception e) {
                    LOG.error("Something went wrong while running tests: " + e, e);
                    updateMessage("Exception has occured while running test.");
                    return null;
                }
            }
        };
        progressBar.progressProperty().bind(runTask.progressProperty());
        progressLabel.textProperty().bind(runTask.messageProperty());
        progressBarBox.setVisible(true);
        new Thread(runTask).start();
    }

    private String shownHtmlOutput = null;
    private ApiTestResult shownResult = null;
    public void saveHtml() {
        if (shownHtmlOutput == null) return;

        FileChooser fc = new FileChooser();
        //set the default open dir
        if (System.getProperty("user.home") != null)
            fc.setInitialDirectory(new File(System.getProperty("user.home")));
        fc.setInitialFileName("result-" + testClass.getName() + ".html");
        File file = fc.showSaveDialog(null);

        if (file == null)
            return;

        try {
            IOUtils.stringToFile(file, shownHtmlOutput);
        } catch (Exception e) {
            LOG.error("Error while writing HTML to file: ", e);
            throw e;
        }
    }
    public void saveXml() {
        if (shownResult == null) return;

        FileChooser fc = new FileChooser();
        //set the default open dir
        if (System.getProperty("user.home") != null)
            fc.setInitialDirectory(new File(System.getProperty("user.home")));
        fc.setInitialFileName("result-overview-" + testClass.getName() + ".xml");
        File file = fc.showSaveDialog(null);

        if (file == null)
            return;

        try {
            shownResult.toXmlOverview(file, true/*showSecurityRisk*/);
        } catch (Exception e) {
            LOG.error("Error while generating XML overview: ", e);
            throw e;
        }
    }

    @FXML
    private void showLogHistory() {
        if (historyStage == null) {
            try {
                LogHistoryPanel logHistoryPanel = logHistoryPanelProvider.get();
                logHistoryPanel.setApiCallHistory(apiCallHistory);

                historyStage = new Stage();
                historyStage.setTitle("Call Log History");
                Scene scene = new Scene(logHistoryPanel);
                historyStage.setScene(scene);
//                historyStage.sizeToScene();
                historyStage.setWidth(800.0);
                historyStage.setHeight(600.0);
            } catch (Exception e) {
                LOG.error("Error while loading Call Log History window", e);
            }
        }

        assert historyStage != null;

        historyStage.show();
    }


    public void resetArguments() {
        for (Map.Entry<String, CommandArgumentChooser> e : extraArguments.entrySet()) {
            String key = e.getKey();
            CommandArgumentChooser chooser = e.getValue();
            if (chooser instanceof StringArgumentChooser)
                ((StringArgumentChooser) chooser).setValue("");
            if (chooser instanceof MultiLineStringArgumentChooser)
                ((MultiLineStringArgumentChooser) chooser).setValue("");
        }
    }
    private File AUTOMATED_TESTER_STORED_ARGUMENTS_FILE = new File(JFedUtils.getUserDataDirectoryFile(), "saved_automated_tester_arguments.properties");
    public void saveArguments() throws IOException {
        final Properties props = new Properties();
        for (Map.Entry<String, CommandArgumentChooser> e : extraArguments.entrySet()) {
            String key = e.getKey();
            CommandArgumentChooser chooser = e.getValue();
            if (chooser.valueProperty() != null && !Objects.equals(((String) chooser.valueProperty().getValue()).trim(), ""))
                props.setProperty(key, (String) chooser.valueProperty().getValue());
        }
        FileWriter w = new FileWriter(AUTOMATED_TESTER_STORED_ARGUMENTS_FILE);
        props.store(w, "");
    }
    public void loadArguments() throws IOException {
        Properties prop = new Properties();
        prop.load(new FileReader(AUTOMATED_TESTER_STORED_ARGUMENTS_FILE))
        ;
        for (Map.Entry<String, CommandArgumentChooser> e : extraArguments.entrySet()) {
            String key = e.getKey();
            String newValue = prop.getProperty(key);
            if (newValue == null)
                newValue = "";

            CommandArgumentChooser chooser = e.getValue();
            if (chooser instanceof StringArgumentChooser)
                ((StringArgumentChooser) chooser).setValue(newValue);
            if (chooser instanceof MultiLineStringArgumentChooser)
                ((MultiLineStringArgumentChooser) chooser).setValue(newValue);

        }
    }
}
