// <copyright file="PopupWindowFinder.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using System;
using System.Collections.ObjectModel;
using System.Linq;

namespace OpenQA.Selenium.Support.UI;

/// <summary>
/// Provides a mechanism by which the window handle of an invoked
/// popup browser window may be determined.
/// </summary>
/// <example>
/// <code>
/// // Store the current window handle so you can switch back to the
/// // original window when you close the popup.
/// string current = driver.CurrentWindowHandle;
/// PopupWindowFinder finder = new PopupWindowFinder(driver);
/// string newHandle = finder.Click(driver.FindElement(By.LinkText("Open new window")));
/// driver.SwitchTo.Window(newHandle);
/// </code>
/// </example>
public class PopupWindowFinder
{
    private readonly IWebDriver driver;
    private readonly TimeSpan timeout;
    private readonly TimeSpan sleepInterval;

    /// <summary>
    /// Initializes a new instance of the <see cref="PopupWindowFinder"/> class.
    /// </summary>
    /// <param name="driver">The <see cref="IWebDriver"/> instance that is used
    /// to manipulate the popup window.</param>
    /// <remarks>When using this constructor overload, the timeout will be 5 seconds,
    /// and the check for a new window will be performed every 250 milliseconds.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="driver"/> is <see langword="null"/>.</exception>
    public PopupWindowFinder(IWebDriver driver)
        : this(driver, DefaultTimeout, DefaultSleepInterval)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PopupWindowFinder"/> class
    /// with the specified timeout.
    /// </summary>
    /// <param name="driver">The <see cref="IWebDriver"/> instance that is used
    /// to manipulate the popup window.</param>
    /// <param name="timeout">The <see cref="TimeSpan"/> representing the amount of
    /// time to wait for the popup window to appear.</param>
    /// <remarks>When using this constructor overload, the check for a new window
    /// will be performed every 250 milliseconds.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="driver"/> is <see langword="null"/>.</exception>
    public PopupWindowFinder(IWebDriver driver, TimeSpan timeout)
        : this(driver, timeout, DefaultSleepInterval)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PopupWindowFinder"/> class
    /// with the specified timeout and using the specified interval to check for
    /// the existence of the new window.
    /// </summary>
    /// <param name="driver">The <see cref="IWebDriver"/> instance that is used
    /// to manipulate the popup window.</param>
    /// <param name="timeout">The <see cref="TimeSpan"/> representing the amount of
    /// time to wait for the popup window to appear.</param>
    /// <param name="sleepInterval">The <see cref="TimeSpan"/> representing the
    /// amount of time to wait between checks of the available window handles.</param>
    /// <exception cref="ArgumentNullException">If <paramref name="driver"/> is <see langword="null"/>.</exception>
    public PopupWindowFinder(IWebDriver driver, TimeSpan timeout, TimeSpan sleepInterval)
    {
        this.driver = driver ?? throw new ArgumentNullException(nameof(driver));
        this.timeout = timeout;
        this.sleepInterval = sleepInterval;
    }

    private static TimeSpan DefaultTimeout => TimeSpan.FromSeconds(5);

    private static TimeSpan DefaultSleepInterval => TimeSpan.FromMilliseconds(250);

    /// <summary>
    /// Clicks on an element that is expected to trigger a popup browser window.
    /// </summary>
    /// <param name="element">The <see cref="IWebElement"/> that, when clicked, invokes
    /// the popup browser window.</param>
    /// <returns>The window handle of the popup browser window.</returns>
    /// <exception cref="WebDriverTimeoutException">Thrown if no popup window appears within the specified timeout.</exception>
    /// <exception cref="ArgumentNullException">Thrown if the element to click is <see langword="null"/>.</exception>
    public string Click(IWebElement element)
    {
        if (element is null)
        {
            throw new ArgumentNullException(nameof(element), "element cannot be null");
        }

        return this.Invoke(element.Click);
    }

    /// <summary>
    /// Invokes a method that is expected to trigger a popup browser window.
    /// </summary>
    /// <param name="popupMethod">An <see cref="Action"/> that, when run, invokes
    /// the popup browser window.</param>
    /// <returns>The window handle of the popup browser window.</returns>
    /// <exception cref="WebDriverTimeoutException">Thrown if no popup window appears within the specified timeout.</exception>
    /// <exception cref="ArgumentNullException">Thrown if the action to invoke is <see langword="null"/>.</exception>
    public string Invoke(Action popupMethod)
    {
        if (popupMethod is null)
        {
            throw new ArgumentNullException(nameof(popupMethod), "popupMethod cannot be null");
        }

        ReadOnlyCollection<string> existingHandles = this.driver.WindowHandles;
        popupMethod();
        WebDriverWait wait = new WebDriverWait(SystemClock.Instance, this.driver, this.timeout, this.sleepInterval);
        string popupHandle = wait.Until(driver =>
        {
            ReadOnlyCollection<string> newHandles = driver.WindowHandles;
            return newHandles.Except(existingHandles, StringComparer.Ordinal).FirstOrDefault();
        });

        return popupHandle;
    }
}
