package cz.moneta.test.harness.connectors.web;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import cz.moneta.test.harness.connectors.rest.SimpleRestConnector;
import cz.moneta.test.harness.exception.HarnessConfigurationException;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.selenium.ClasspathFileDetector;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.*;
import org.openqa.selenium.logging.*;
import org.openqa.selenium.manager.SeleniumManager;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static cz.moneta.test.harness.constants.HarnessConfigConstants.SELENIUM_DOWNLOAD_DIRECTORY;

public class DriverUtils {

    protected static final Set<String> LOG_TYPES = Stream.of(LogType.BROWSER, LogType.CLIENT, LogType.DRIVER,
            LogType.PERFORMANCE, LogType.PROFILER, LogType.SERVER).collect(Collectors.toSet());
    private static final Logger logger = LogManager.getLogger(DriverUtils.class);
    private final SeleniumWebConnector connector;

    public DriverUtils(SeleniumWebConnector connector) {
        this.connector = connector;
    }

    public String takeSnapshot(String snapshotFileName) {
        String snapshotsPath = connector.getStore().getConfig("selenium.snapshots.path", "target/screenshots");
        File file = Paths.get(snapshotsPath, snapshotFileName).toFile();
        try {
            FileUtils.writeByteArrayToFile(file, ((TakesScreenshot) connector.getDriver()).getScreenshotAs(OutputType.BYTES));
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write snapshot " + snapshotFileName + " to " + snapshotsPath, e);
        } catch (WebDriverException e2) { // An unexpected error swallows why the test failed
            logger.error("Error during getting snapshot: " + e2.getMessage());
            e2.printStackTrace();
        }
        return file.getAbsolutePath();
    }

    public String captureLog(String logFileName, String logType) {
        String path = connector.getStore().getConfig("selenium.logs.path", "target/logs");
        Logs logs = connector.getDriver()
                .manage()
                .logs();

        LogEntries logEntriesForType = null;
        try {
            logEntriesForType = logs.get(logType);
        } catch (Exception wde) {
            logger.warn(() -> "We were not able to get log entries for log type: " + logType);
        }
        if (logEntriesForType != null) {
            List<LogEntry> logEntries = logEntriesForType.getAll();
            try {
                File file = Paths.get(path, logFileName, ".log").toFile();
                FileUtils.writeLines(file, logEntries);
                return file.getAbsolutePath();
            } catch (IOException e) {
                throw new IllegalStateException("Unable to write log " + logFileName + " to " + path, e);
            }
        }
        return null;
    }

    public void captureDom(String domFileName) {
        String path = connector.getStore().getConfig("selenium.dom.path", "target/dom");
        File file = Paths.get(path, domFileName).toFile();
        try {
            FileUtils.write(file, connector.getDriver().getPageSource());
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write " + domFileName + " to " + path, e);
        } catch (WebDriverException e2) { // An unexpected error swallows why the test failed
            logger.error("Error during getting DOM: " + e2.getMessage());
            e2.printStackTrace();
        }
    }

    /**
     * Add platform/browser/driver independent capabilities.
     *
     * @return DesiredCapabilities with default capabilities added
     */
    protected void addDefaultDesiredCapabilities(MutableCapabilities capabilities) {
        LoggingPreferences logs = new LoggingPreferences();
        LOG_TYPES.forEach(lt -> logs.enable(lt, Level.ALL));
        //capabilities.setCapability(CapabilityType.LOGGING_PREFS, logs); // TODO: vyřešit

        java.util.logging.Logger.getLogger(RemoteWebDriver.class.getName()).setLevel(Level.INFO);
        java.util.logging.Logger.getLogger(SeleniumManager.class.getName()).setLevel(Level.INFO);
    }

    /**
     * Allows to configure additional capabilities.
     *
     * @param capabilities collection of capabilities to be added to
     * @param key          key to use for capabilities lookup in configuration
     */
    protected void addAdditionalDesiredCapabilities(MutableCapabilities capabilities, String key) {
        String additionalCapabilities = connector.getStore().getConfig(key);
        if (additionalCapabilities != null) {
            Arrays.stream(additionalCapabilities.split(",")).forEach(
                    (item) -> {
                        String[] keyValue = item.split("=");
                        String value = keyValue[1];
                        if (!keyValue[0].equals("applicationName")) {
                            if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
                                capabilities.setCapability(keyValue[0], Boolean.parseBoolean(value));
                            } else {
                                capabilities.setCapability(keyValue[0], value);
                            }
                        }
                    }
            );
        }
    }

    WebDriver initializeRemoteWebDriver(String hubUrl) {
        try {
            // TODO: timeout připojení k browseru
            // HttpClient.Factory factory = HttpClient.Factory.createDefault();
            // HttpClient.Builder builder = factory.builder()
            //        .connectionTimeout(Duration.ofSeconds(10));

            // HttpClient.Factory clientFactory = new HttpClient.Factory() {
            //    @Override
            //    public HttpClient.Builder builder() {
            //        return new HttpClient.Builder() {
            //            @Override
            //            public HttpClient createClient(URL url) {
            //                return builder.createClient(url);
            //            }
            //        };
            //    }

            //    @Override
            //    public void cleanupIdleClients() {
            //        factory.cleanupIdleClients();
            //    }
            // };

            // HttpCommandExecutor executor = new HttpCommandExecutor(new HashMap<>(), new URL(hubUrl), clientFactory);
            // RemoteWebDriver remoteWebDriver = new RemoteWebDriver(executor, connector.getDesiredRemoteCapabilities());

            RemoteWebDriver remoteWebDriver = new RemoteWebDriver(new URL(hubUrl), connector.getDesiredRemoteCapabilities());

            remoteWebDriver.setFileDetector(new ClasspathFileDetector());
            String sessionId = remoteWebDriver.getSessionId().toString();
            logger.info(() -> String.format("New Selenium web driver session %s started at node %s", sessionId, getNodeUrl(hubUrl, sessionId)));
            return remoteWebDriver;
        } catch (Exception e) {
            logger.error(() -> "Failed to initialize remote web driver with url " + hubUrl, e);
            return null;
        }
    }

    private String getNodeUrl(String hubUrl, String sessionId) {
        HttpURLConnection gridApiConnection = null;
        try {
            URL url = new URL(hubUrl);
            URL gridGraphQLUrl = new URL(String.format("http://%s:%d/graphql", url.getHost(), url.getPort()));
            Map<String, Object> headers = new HashMap<>();
            headers.put("Content-Type", "application/json");
            Pair<Integer, String> response = new SimpleRestConnector(gridGraphQLUrl.toString(), "SeleniumGraphQL")
                    .post("",
                            Entity.json("{\"query\":\"{ session (id: \\\"" + sessionId + "\\\") { uri, nodeUri } } \"}"),
                            new GenericType<>(String.class),
                            headers);
            JsonObject gridResponse = JsonParser.parseString(response.getRight().toString()).getAsJsonObject();
            String nodeUrlString = gridResponse.getAsJsonObject("data").getAsJsonObject("session").get("uri").getAsString();
            URL nodeUrl = new URL(nodeUrlString);
            this.connector.setNodeHost(nodeUrl.getHost());
            return nodeUrlString;
        } catch (Exception e) {
            throw new HarnessException(String.format("Failed to get node info for session %s", sessionId), e);
        } finally {
            if (gridApiConnection != null) {
                try {
                    gridApiConnection.disconnect();
                } catch (Exception e) {
                    throw new HarnessException("Could not close connection to grid API.", e);
                }
            }
        }
    }

    public boolean checkIfFileExists(String directory, String filename, int waitInSeconds, boolean deleteFile, boolean useDirectoryListMethod) {
        boolean fileExists = false;
        // If no directory set as input parameter then get it from config file or System properties.
        if (directory == null) {
            directory = Stream.of(Optional.ofNullable(System.getProperty(SELENIUM_DOWNLOAD_DIRECTORY)),
                            Optional.ofNullable(connector.getStore().getConfig(SELENIUM_DOWNLOAD_DIRECTORY)))
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .findFirst()
                    .orElseThrow(() -> new HarnessConfigurationException(("You need to set download directory or configure " + SELENIUM_DOWNLOAD_DIRECTORY + " parameter!")));
        }

        // File which we try to find
        File fileToCheck = FileUtils.getFile(directory, filename);
        File folderToCheck = new File(directory);

        try {
            // Checking if file is downloaded. Try it every second to "waitInSeconds" limit.
            for (int i = 0; i <= waitInSeconds; i++) {
                // Simulating wait for file download.
                Thread.sleep(TimeUnit.SECONDS.toMillis(1));

                if (useDirectoryListMethod) {
                    logger.debug("Checking if file: {} present in directory list: {}.", filename, directory);
                    fileExists = Arrays.stream(folderToCheck.list()).anyMatch(filename::equals);
                } else {
                    logger.debug("Checking if file: {} exists in directory: {}.", filename, directory);
                    fileExists = (fileToCheck.exists() || fileToCheck.isFile());
                }

                if (fileExists) break;
            }

            // When file exists - log it and delete file
            if (fileExists) {
                logger.debug("Found file: {}.", fileToCheck.getAbsolutePath());
                logger.info("Found expected file: {} in directory {}.", filename, directory);
                // For cleaning purposes. (Does not delete all files, ex. in case of wrong filename the downloaded file will remain in directory.)
                if (deleteFile) {
                    logger.debug("Deleting file: {}.", fileToCheck.getAbsolutePath());
                    Files.delete(fileToCheck.toPath());
                }
            } else {
                throw new HarnessException(String.format("Could not find expected file: %s in directory %s withing the specified timeout", filename, directory));
            }
        } catch (Exception e) {
            throw new HarnessException(String.format("Something went wrong: Could not find expected file: %s in directory %s.", filename, directory), e);
        }
        return fileExists;
    }
}
