using ESuite.UI.E2E.Pages;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using SeleniumExtras.WaitHelpers;

namespace ESuite.UI.E2E.Helpers
{
    public static class I
    {
        private readonly static ConfigHelper configHelper = new();
        private readonly static int TimeToWait = configHelper.ClickWaitSeconds;
        public static void Click(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            IWebElement? element = WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver) ?? throw new NoSuchElementException($"Element with locator '{locator}' was not found.");

            try
            {
                element.Click();
            }
            catch (Exception)
            {
                IWebElement? newPositionElement = WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver);
                newPositionElement!.Click();
            }
        }

        public static void RefreshPage(IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();

            driver!.Navigate().Refresh();
        }

        public static IWebElement? WaitForVisible(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            return Retry((int)waitTimeInSeconds, () =>
            {
                return wait.Until(ExpectedConditions.ElementIsVisible(locator));
            });
        }

        private static T Retry<T>(int waitTimeInSeconds, Func<T> func)
        {
            var expire = DateTime.Now.AddSeconds((int)waitTimeInSeconds);
            Exception? exception = null;

            while (DateTime.Now < expire)
            {
                try
                {
                    return func();
                }
                catch (Exception e)
                {
                    exception = e;
                }
            }
            throw new Exception("Retry failed", exception);
        }

        private static void Retry(int waitTimeInSeconds, Action func)
        {
            var expire = DateTime.Now.AddSeconds((int)waitTimeInSeconds);
            Exception? exception = null;

            while (DateTime.Now < expire)
            {
                try
                {
                    func();
                    exception = null;

                    break;
                }
                catch (Exception e)
                {
                    exception = e;
                }

            }

            if (exception != null)
            {
                throw new Exception("Retry failed", exception);
            }
        }

        public static IWebElement? WaitForElementVisibleAndClickable(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            return Retry((int)waitTimeInSeconds, () =>
            {
                WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));
                try
                {
                    wait.Until(ExpectedConditions.ElementIsVisible(locator));
                    return wait.Until(ExpectedConditions.ElementToBeClickable(locator));
                }
                catch (StaleElementReferenceException)
                {
                    throw;
                }
                catch (NoSuchElementException)
                {
                    return null;
                }
            });
        }

        public static IWebElement? WaitToBeClickable(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            return Retry((int)waitTimeInSeconds, () =>
            {
                try
                {
                    return wait.Until(ExpectedConditions.ElementToBeClickable(locator));
                }
                catch (StaleElementReferenceException)
                {
                    throw;
                }
                catch (NoSuchElementException)
                {
                    return null;
                }
            });
        }

        public static bool WaitTillInvisible(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            try
            {
                return wait.Until(ExpectedConditions.InvisibilityOfElementLocated(locator));
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }

        public static void FillField(By locator, string text, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();
            IWebElement? element = WaitToBeClickable(locator, (int)waitTimeInSeconds, driver);

            if (element != null)
            {
                element.Clear();
                DebounceDelay(300);
                element.SendKeys(text);

                if (element.TagName.Equals("input", StringComparison.CurrentCultureIgnoreCase))
                {
                    WaitForElementWithAttributeValue(locator, "value", text);
                }
            }
            else
            {
                throw new InvalidOperationException($"There is not input field with locator: {locator}");
            }
        }

        public static void WaitForElementWithAttributeValue(By locator, string attributeName, string expectedValue, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            try
            {
                wait.Until(driver =>
                {
                    var element = driver.FindElement(locator);
                    var actualValue = element.GetAttribute(attributeName);

                    return actualValue == expectedValue;
                });
            }
            catch (WebDriverTimeoutException)
            {
                throw new TimeoutException($"Element with locator '{locator}', attribute '{attributeName}' and expected value '{expectedValue}' was not found!");
            }
        }

        public static void WaitForOptionToBePresentInSelect(By selectLocator, string optionText, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            BasicPage basicPage = new(driver);
            WaitTillInvisible(BasicPage.LoadingDropdownMessage);

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            wait.Until(driver =>
            {
                var selectElement = driver.FindElement(selectLocator);
                var option = selectElement.FindElements(By.XPath($"./option[text()='{optionText}']"));

                return option.Count > 0;
            });
        }

        public static void SelectOption(By locator, object option, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();
            string? optionString = option.ToString();

            Retry((int)waitTimeInSeconds, () =>
            {
                IWebElement? element = WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver);
                WaitForOptionToBePresentInSelect(locator, optionString!);

                try
                {
                    SelectElement select = new(element!);
                    select.SelectByText(optionString!);
                }
                catch (StaleElementReferenceException)
                {
                    throw;
                }
                catch (Exception e)
                {
                    throw new NoSuchElementException($"Option '{option}' was not found. Exception: {e.Message}");
                }
            });
        }

        public static string GetText(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            try
            {
                waitTimeInSeconds ??= TimeToWait;
                driver ??= WebDriverHelper.GetWebDriver();
                IWebElement? element = WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver);

                return element!.Text;
            }
            catch (NoSuchElementException)
            {
                throw new NoSuchElementException($"I can't find the text for a non-existent locator: {locator}");
            }
        }

        public static bool CheckIfTextInChildElements(By locator, string searchText, IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();

            IWebElement? parentElement = driver!.FindElement(locator);
            IReadOnlyCollection<IWebElement> childElements = parentElement.FindElements(By.XPath(".//*"));

            foreach (IWebElement child in childElements)
            {
                if (child.Text.Contains(searchText)) return true;
            }

            return false;
        }

        public static bool CheckIfTextIs(By locator, string searchText, IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();
            IWebElement? element = driver!.FindElement(locator);
            if (element.Text.Contains(searchText)) return true;

            return false;
        }

        public static string GetValueAttribute(By locator, int? waitTimeInSeconds = null, string? attributeName = "value", IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            IWebElement? element = WaitForVisible(locator, (int)waitTimeInSeconds, driver);

            return element!.GetAttribute(attributeName!)!;
        }

        public static string GetTextFromElement(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            try
            {
                waitTimeInSeconds ??= TimeToWait;
                driver ??= WebDriverHelper.GetWebDriver();

                IWebElement? element = WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver);

                return element!.Text;
            }
            catch (NoSuchElementException)
            {
                throw new NoSuchElementException($"I can't find elements for a non-existent locator: {locator}");
            }
        }

        public static IList<string> GetTextsFromElements(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            try
            {
                waitTimeInSeconds ??= TimeToWait;
                driver ??= WebDriverHelper.GetWebDriver();
                WaitForElementVisibleAndClickable(locator, (int)waitTimeInSeconds, driver);
                var elements = driver.FindElements(locator);

                IList<string> texts = [];

                foreach (var element in elements)
                {
                    texts.Add(element.Text);
                }

                return texts;
            }
            catch (NoSuchElementException)
            {
                throw new NoSuchElementException($"I can't find elements for a non-existent locator: {locator}");
            }
        }

        public static int CountElementsOnPage(By locator, IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();
            System.Collections.ObjectModel.ReadOnlyCollection<IWebElement> webElements = driver.FindElements(locator);

            return ((IReadOnlyCollection<IWebElement>)webElements).Count;
        }

        public static bool IsVisibleNumberOfElements(By locator, int numberOfElements, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            try
            {
                return wait.Until(drv => drv.FindElements(locator).Count == numberOfElements);
            }
            catch (WebDriverTimeoutException)
            {
                return false;
            }
        }

        /// <summary>
        /// Delays the execution for the specified amount of time in milliseconds.
        /// </summary>
        /// <param name="time">The time to wait.</param>
        public static void DebounceDelay(int time)
        {
            Thread.Sleep(time);
        }

        public static void WaitForPageToLoad(int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();
            waitTimeInSeconds ??= TimeToWait;
            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            wait.Until(driver => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState")!.Equals("complete"));
        }

        public static void NavigateToURL(string url, IWebDriver? driver = null)
        {
            driver ??= WebDriverHelper.GetWebDriver();
            driver.Navigate().GoToUrl(url);
            WaitForPageToLoad();
        }

        public static bool WaitForElementToBeChecked(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            try
            {
                return wait.Until(ExpectedConditions.ElementToBeSelected(locator));
            }
            catch (Exception)
            {
                return false;
            }
        }

        public static String? ReturnSelectedOption(By locator, int? waitTimeInSeconds = null, IWebDriver? driver = null)
        {
            waitTimeInSeconds ??= TimeToWait;
            driver ??= WebDriverHelper.GetWebDriver();

            WebDriverWait wait = new(driver, TimeSpan.FromSeconds((int)waitTimeInSeconds));

            try
            {
                return wait.Until(driver =>
                {
                    IWebElement selectElement = driver.FindElement(locator);
                    SelectElement select = new(selectElement);
                    string selectedText = select.SelectedOption.Text;

                    return selectedText;
                });
            }
            catch (WebDriverTimeoutException)
            {
                return null;
                throw new TimeoutException($"Element with locator '{locator}' was not found!");
            }
        }
    }
}
