package old.core;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.logging.Level;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.remote.AbstractDriverOptions;
import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import static old.core.BasePage.getErrorBy;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * @author gulnaz
 */
public abstract class BaseTest {
  protected static final String gridUrl;
  protected static final String applicationUrl;

  private static final Logger LOGGER = LoggerFactory.getLogger(BaseTest.class);
  private static final String UNDERSCORE = "_";
  private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(20);
  private static final Duration TIMEOUT = Duration.ofSeconds(3);
  private static final int WINDOW_WIDTH = 945;

  @RegisterExtension
  protected ScreenWatcher screenWatcher = new ScreenWatcher();
  protected WebDriver driver;
  protected AbstractDriverOptions options;

  private boolean closeBrowser = true;
  private TestInfo testInfo;

  static {
    gridUrl = System.getProperty("selenium.grid.url");
    applicationUrl = System.getProperty("application.url");
    LOGGER.info("application.url: {}", applicationUrl);
    LOGGER.info("grid.url: {}", gridUrl);
  }

  @BeforeEach
  void beforeEach(TestInfo testInfo) {
    this.testInfo = testInfo;
  }

  @AfterEach
  void tearDown() {
    if (closeBrowser && driver != null) {
      driver.quit();
    }
  }

  public static void sleep(long millis) {
    try {
      Thread.sleep(millis);
    }
    catch (InterruptedException e) {
      throw new RuntimeException("Something went wrong...", e);
    }
  }

  public URL getGridURL() throws MalformedURLException {
    return new URL(gridUrl);
  }

  protected void initDriver(Browser browser) {

    System.setProperty("webdriver.http.factory", "jdk-http-client");
    try {
      options = browser.getOptions().newInstance();
    }
    catch (InstantiationException | IllegalAccessException e) {
      throw new RuntimeException("Failed to create capabilities", e);
    }

    options.setCapability("selenoid:options", new HashMap<String, Object>() {
      {
        put("enableLog", Boolean.valueOf(System.getProperty("enableLog")));
        put("enableVNC", Boolean.valueOf(System.getProperty("enableVNC")));
        put("enableVideo", Boolean.valueOf(System.getProperty("enableVideo")));
        put("sessionTimeout", "3m");
        put("logName", getFileName(testInfo.getTestClass(), testInfo.getTestMethod(), testInfo.getDisplayName() + ".log"));
        put("videoName", getFileName(testInfo.getTestClass(), testInfo.getTestMethod(), testInfo.getDisplayName() + ".mp4"));
      }
    });

    if (System.getProperty("enableLog") != null && browser == Browser.CHROME) {
      LoggingPreferences logPrefs = new LoggingPreferences();
      logPrefs.enable(LogType.BROWSER, Level.ALL);
      logPrefs.enable(LogType.CLIENT, Level.ALL);
      logPrefs.enable(LogType.DRIVER, Level.ALL);
      logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
      logPrefs.enable(LogType.PROFILER, Level.ALL);
      logPrefs.enable(LogType.SERVER, Level.ALL);

      options.setCapability("goog:loggingPrefs", logPrefs);
    }
    
    LOGGER.info("try to create RemoteWebDriver, "  + testInfo.getDisplayName());

    int attempts = 0;
    while (attempts++ < 5) {

      try {
        URL gridUrl = getGridURL();
        driver = new RemoteWebDriver(gridUrl, options);
        break;
      }
      catch (WebDriverException ignored) {
        LOGGER.info("The driver did not start on " + attempts + " attempt");
      }
      catch (MalformedURLException e) {
        throw new RuntimeException("Invalid grid URL", e);
      }
    }

    if (driver == null) {
      throw new RuntimeException("Failed to create driver for " + testInfo.getDisplayName());
    }

    LOGGER.info("RemoteWebDriver created, " + testInfo.getDisplayName());

    // Configure to upload local files to remote Selenium instance
    ((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector());

    driver.manage().window().maximize();
    //TODO после обновления selenium (4.9.0 <) необходимо перепроверить maximize()
    if (getWindowWidth(driver) < WINDOW_WIDTH) { //.maximize() не срабатывает с первого раза
      driver.manage().window().maximize();
    }
    driver.manage().window().maximize();
    driver.get(applicationUrl);
    assertTrue(waitForCorrectWindowWidth(),
        String.format("browser window size is less than and not equal to as expected: expected - %dpx, actual - %dpx",
            WINDOW_WIDTH, driver.manage().window().getSize().getWidth()));
    assertTrue(getExplicitWait(TIMEOUT)
            .until(ExpectedConditions.invisibilityOfElementLocated(getErrorBy())),
        "an error occurred while opening application");
  }

  private int getWindowWidth(WebDriver webDriver) {
    return webDriver.manage().window().getSize().getWidth();
  }

  protected Duration getDefaultTimeout() {
    return DEFAULT_TIMEOUT;
  }

  protected WebDriverWait getExplicitWait(Duration timeout) {
    return new WebDriverWait(driver, timeout);
  }

  protected void setCloseBrowser(boolean closeBrowser) {
    this.closeBrowser = closeBrowser;
  }

  private boolean waitForCorrectWindowWidth() {
    try {
      return getExplicitWait(TIMEOUT)
          .until(webDriver -> webDriver.manage().window().getSize().getWidth() >= WINDOW_WIDTH);
    }
    catch (TimeoutException ignored) {
      return false;
    }
  }

  private String getFileName(Optional<Class<?>> testClass, Optional<Method> testMethod, String displayName) {
    String fileName = "";
    if (testClass.isPresent()) {
      fileName = testClass.get().getSimpleName() + UNDERSCORE;
    }

    if (testMethod.isPresent()) {
      fileName += testMethod.get().getName() + UNDERSCORE;
    }

    fileName += displayName;

    return fileName;
  }

  private String getFileName(ExtensionContext context) {
    return getFileName(context.getTestClass(), context.getTestMethod(), context.getDisplayName());
  }

  public class ScreenWatcher implements AfterTestExecutionCallback  {
    private static final String PATH = "target";
    private static final String EXTENSION = "jpg";

    private File scrFile;

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
      LOGGER.info("afterTestExecution");
      boolean failed = context.getExecutionException().isPresent();

      if (!failed) {
        if (scrFile != null)
          scrFile.delete();   
      }
      else {
        LOGGER.info("test failed");
        if (driver == null)
          return;
        saveScreen(getFileName(context));

        for (String type : driver.manage().logs().getAvailableLogTypes()) {
          List<LogEntry> logEntries = driver.manage().logs().get(type).getAll();

          if (logEntries.isEmpty()) {
            continue;
          }

          LOGGER.info("Logging browser messages for type: " + type);
          for (LogEntry entry : logEntries) {
            LocalDateTime localdateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(entry.getTimestamp()), TimeZone.getDefault().toZoneId());
            String dateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(localdateTime);
            LOGGER.warn(dateTime + " " + entry.getLevel() + " " + entry.getMessage());
          }
          LOGGER.info("Logged browser messages for type: " + type);
        }
      }
    }

    public void saveScreen(String fileName) {
      try {
        TakesScreenshot takesScreenshot = (TakesScreenshot) driver;
        scrFile = takesScreenshot.getScreenshotAs(OutputType.FILE);
        Path destFile = Paths.get("", PATH, fileName + "." + EXTENSION);
        Files.copy(scrFile.toPath(), destFile, StandardCopyOption.REPLACE_EXISTING);
      }
      catch (IOException e) {
        throw new RuntimeException("Failed to capture screen", e);
      }
    }
  }
}
