// <copyright file="FirefoxOptions.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.Generic;

namespace OpenQA.Selenium.Firefox;

/// <summary>
/// Class to manage options specific to <see cref="FirefoxDriver"/>
/// </summary>
/// <remarks>
/// Used with the marionette executable wires.exe.
/// </remarks>
/// <example>
/// <code>
/// FirefoxOptions options = new FirefoxOptions();
/// </code>
/// <para></para>
/// <para>For use with FirefoxDriver:</para>
/// <para></para>
/// <code>
/// FirefoxDriver driver = new FirefoxDriver(options);
/// </code>
/// <para></para>
/// <para>For use with RemoteWebDriver:</para>
/// <para></para>
/// <code>
/// RemoteWebDriver driver = new RemoteWebDriver(new Uri("http://localhost:4444/wd/hub"), options.ToCapabilities());
/// </code>
/// </example>
public class FirefoxOptions : DriverOptions
{
    private const string BrowserNameValue = "firefox";

    private const string IsMarionetteCapability = "marionette";
    private const string FirefoxLegacyProfileCapability = "firefox_profile";
    private const string FirefoxLegacyBinaryCapability = "firefox_binary";
    private const string FirefoxProfileCapability = "profile";
    private const string FirefoxBinaryCapability = "binary";
    private const string FirefoxArgumentsCapability = "args";
    private const string FirefoxLogCapability = "log";
    private const string FirefoxPrefsCapability = "prefs";
    private const string FirefoxEnvCapability = "env";
    private const string FirefoxOptionsCapability = "moz:firefoxOptions";
    private const string FirefoxEnableDevToolsProtocolCapability = "moz:debuggerAddress";
    private readonly List<string> firefoxArguments = new List<string>();
    private readonly Dictionary<string, object> profilePreferences = new Dictionary<string, object>();
    private readonly Dictionary<string, object> additionalFirefoxOptions = new Dictionary<string, object>();
    private readonly Dictionary<string, object> environmentVariables = new Dictionary<string, object>();

    /// <summary>
    /// Initializes a new instance of the <see cref="FirefoxOptions"/> class.
    /// </summary>
    public FirefoxOptions()
        : base()
    {
        this.BrowserName = BrowserNameValue;
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxOptionsCapability, "current FirefoxOptions class instance");
        this.AddKnownCapabilityName(FirefoxOptions.IsMarionetteCapability, "UseLegacyImplementation property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxProfileCapability, "Profile property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxBinaryCapability, "BrowserExecutableLocation property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxArgumentsCapability, "AddArguments method");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxPrefsCapability, "SetPreference method");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxEnvCapability, "SetEnvironmentVariable method");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxLogCapability, "LogLevel property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxLegacyProfileCapability, "Profile property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxLegacyBinaryCapability, "BrowserExecutableLocation property");
        this.AddKnownCapabilityName(FirefoxOptions.FirefoxEnableDevToolsProtocolCapability, "EnableDevToolsProtocol property");
        // https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
        // Enable BiDi only
        this.SetPreference("remote.active-protocols", 1);
    }

    /// <summary>
    /// Gets or sets the <see cref="FirefoxProfile"/> object to be used with this instance.
    /// </summary>
    public FirefoxProfile? Profile { get; set; }

    /// <summary>
    /// Gets or sets the path and file name of the Firefox browser executable.
    /// </summary>
    public override string? BinaryLocation { get; set; }

    /// <summary>
    /// Gets or sets the path and file name of the Firefox browser executable.
    /// </summary>
    [Obsolete("Use BinaryLocation property instead of BrowserExecutableLocation. This one will be removed soon.")]
    public string? BrowserExecutableLocation
    {
        get => this.BinaryLocation;
        set => this.BinaryLocation = value;
    }

    /// <summary>
    /// Gets or sets the logging level of the Firefox driver.
    /// </summary>
    public FirefoxDriverLogLevel LogLevel { get; set; } = FirefoxDriverLogLevel.Default;

    /// <summary>
    /// Gets or sets a value indicating whether to enable the DevTools protocol for the launched browser.
    /// </summary>
    public bool EnableDevToolsProtocol { get; set; }

    /// <summary>
    /// Gets or sets the options for automating Firefox on Android.
    /// </summary>
    public FirefoxAndroidOptions? AndroidOptions { get; set; }

    /// <summary>
    /// Adds an argument to be used in launching the Firefox browser.
    /// </summary>
    /// <param name="argumentName">The argument to add.</param>
    /// <remarks>Arguments must be preceded by two dashes ("--").</remarks>
    /// <exception cref="ArgumentException">If <paramref name="argumentName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void AddArgument(string argumentName)
    {
        if (string.IsNullOrEmpty(argumentName))
        {
            throw new ArgumentException("argumentName must not be null or empty", nameof(argumentName));
        }

        this.AddArguments(argumentName);
    }

    /// <summary>
    /// Adds a list arguments to be used in launching the Firefox browser.
    /// </summary>
    /// <param name="argumentsToAdd">An array of arguments to add.</param>
    /// <remarks>Each argument must be preceded by two dashes ("--").</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="argumentsToAdd"/> is <see langword="null"/>.</exception>
    public void AddArguments(params string[] argumentsToAdd)
    {
        this.AddArguments((IEnumerable<string>)argumentsToAdd);
    }

    /// <summary>
    /// Adds a list arguments to be used in launching the Firefox browser.
    /// </summary>
    /// <param name="argumentsToAdd">An array of arguments to add.</param>
    /// <exception cref="ArgumentNullException">If <paramref name="argumentsToAdd"/> is <see langword="null"/>.</exception>
    public void AddArguments(IEnumerable<string> argumentsToAdd)
    {
        if (argumentsToAdd == null)
        {
            throw new ArgumentNullException(nameof(argumentsToAdd), "argumentsToAdd must not be null");
        }

        this.firefoxArguments.AddRange(argumentsToAdd);
    }

    /// <summary>
    /// Sets a preference in the profile used by Firefox.
    /// </summary>
    /// <param name="preferenceName">Name of the preference to set.</param>
    /// <param name="preferenceValue">Value of the preference to set.</param>
    /// <exception cref="ArgumentException">If <paramref name="preferenceName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetPreference(string preferenceName, bool preferenceValue)
    {
        this.SetPreferenceValue(preferenceName, preferenceValue);
    }

    /// <summary>
    /// Sets a preference in the profile used by Firefox.
    /// </summary>
    /// <param name="preferenceName">Name of the preference to set.</param>
    /// <param name="preferenceValue">Value of the preference to set.</param>
    /// <exception cref="ArgumentException">If <paramref name="preferenceName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetPreference(string preferenceName, int preferenceValue)
    {
        this.SetPreferenceValue(preferenceName, preferenceValue);
    }

    /// <summary>
    /// Sets a preference in the profile used by Firefox.
    /// </summary>
    /// <param name="preferenceName">Name of the preference to set.</param>
    /// <param name="preferenceValue">Value of the preference to set.</param>
    /// <exception cref="ArgumentException">If <paramref name="preferenceName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetPreference(string preferenceName, long preferenceValue)
    {
        this.SetPreferenceValue(preferenceName, preferenceValue);
    }

    /// <summary>
    /// Sets a preference in the profile used by Firefox.
    /// </summary>
    /// <param name="preferenceName">Name of the preference to set.</param>
    /// <param name="preferenceValue">Value of the preference to set.</param>
    /// <exception cref="ArgumentException">If <paramref name="preferenceName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetPreference(string preferenceName, double preferenceValue)
    {
        this.SetPreferenceValue(preferenceName, preferenceValue);
    }

    /// <summary>
    /// Sets a preference in the profile used by Firefox.
    /// </summary>
    /// <param name="preferenceName">Name of the preference to set.</param>
    /// <param name="preferenceValue">Value of the preference to set.</param>
    /// <exception cref="ArgumentException">If <paramref name="preferenceName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetPreference(string preferenceName, string preferenceValue)
    {
        this.SetPreferenceValue(preferenceName, preferenceValue);
    }

    /// <summary>
    /// Sets an environment variable to be set in the operating system's environment under which the Firefox browser is launched.
    /// </summary>
    /// <param name="variableName">The name of the environment variable.</param>
    /// <param name="variableValue">The value of the environment variable.</param>
    /// <exception cref="ArgumentException">If <paramref name="variableName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public void SetEnvironmentVariable(string variableName, string? variableValue)
    {
        if (string.IsNullOrEmpty(variableName))
        {
            throw new ArgumentException("Environment variable name cannot be null or an empty string");
        }

        this.environmentVariables[variableName] = variableValue ?? string.Empty;
    }

    /// <summary>
    /// Provides a means to add additional capabilities not yet added as type safe options
    /// for the Firefox driver.
    /// </summary>
    /// <param name="optionName">The name of the capability to add.</param>
    /// <param name="optionValue">The value of the capability to add.</param>
    /// <exception cref="ArgumentException">
    /// thrown when attempting to add a capability for which there is already a type safe option, or
    /// when <paramref name="optionName"/> is <see langword="null"/> or the empty string.
    /// </exception>
    /// <remarks>Calling <see cref="AddAdditionalFirefoxOption(string, object)"/>
    /// where <paramref name="optionName"/> has already been added will overwrite the
    /// existing value with the new value in <paramref name="optionValue"/>.
    /// Calling this method adds capabilities to the Firefox-specific options object passed to
    /// geckodriver.exe (property name 'moz:firefoxOptions').</remarks>
    public void AddAdditionalFirefoxOption(string optionName, object optionValue)
    {
        this.ValidateCapabilityName(optionName);
        this.additionalFirefoxOptions[optionName] = optionValue;
    }

    /// <summary>
    /// Returns DesiredCapabilities for Firefox with these options included as
    /// capabilities. This does not copy the options. Further changes will be
    /// reflected in the returned capabilities.
    /// </summary>
    /// <returns>The DesiredCapabilities for Firefox with these options.</returns>
    public override ICapabilities ToCapabilities()
    {
        IWritableCapabilities capabilities = GenerateDesiredCapabilities(true);
        Dictionary<string, object> firefoxOptions = this.GenerateFirefoxOptionsDictionary();
        capabilities.SetCapability(FirefoxOptionsCapability, firefoxOptions);
        if (this.EnableDevToolsProtocol)
        {
            capabilities.SetCapability(FirefoxEnableDevToolsProtocolCapability, true);
        }

        return capabilities.AsReadOnly();
    }

    private Dictionary<string, object> GenerateFirefoxOptionsDictionary()
    {
        Dictionary<string, object> firefoxOptions = new Dictionary<string, object>();

        if (this.Profile != null)
        {
            firefoxOptions[FirefoxProfileCapability] = this.Profile.ToBase64String();
        }

        if (!string.IsNullOrEmpty(this.BinaryLocation))
        {
            firefoxOptions[FirefoxBinaryCapability] = this.BinaryLocation!;
        }

        if (this.LogLevel != FirefoxDriverLogLevel.Default)
        {
            Dictionary<string, object> logObject = new Dictionary<string, object>();
            logObject["level"] = this.LogLevel.ToString().ToLowerInvariant();
            firefoxOptions[FirefoxLogCapability] = logObject;
        }

        if (this.firefoxArguments.Count > 0)
        {
            List<object> args = [.. this.firefoxArguments];

            firefoxOptions[FirefoxArgumentsCapability] = args;
        }

        if (this.profilePreferences.Count > 0)
        {
            firefoxOptions[FirefoxPrefsCapability] = this.profilePreferences;
        }

        if (this.environmentVariables.Count > 0)
        {
            firefoxOptions[FirefoxEnvCapability] = this.environmentVariables;
        }

        if (this.AndroidOptions != null)
        {
            AddAndroidOptions(this.AndroidOptions, firefoxOptions);
        }

        foreach (KeyValuePair<string, object> pair in this.additionalFirefoxOptions)
        {
            firefoxOptions.Add(pair.Key, pair.Value);
        }

        return firefoxOptions;
    }

    private void SetPreferenceValue(string preferenceName, object preferenceValue)
    {
        if (string.IsNullOrEmpty(preferenceName))
        {
            throw new ArgumentException("Preference name may not be null an empty string.", nameof(preferenceName));
        }

        this.profilePreferences[preferenceName] = preferenceValue;
    }

    private static void AddAndroidOptions(FirefoxAndroidOptions androidOptions, Dictionary<string, object> firefoxOptions)
    {
        firefoxOptions["androidPackage"] = androidOptions.AndroidPackage;

        if (!string.IsNullOrEmpty(androidOptions.AndroidDeviceSerial))
        {
            firefoxOptions["androidDeviceSerial"] = androidOptions.AndroidDeviceSerial!;
        }

        if (!string.IsNullOrEmpty(androidOptions.AndroidActivity))
        {
            firefoxOptions["androidActivity"] = androidOptions.AndroidActivity!;
        }

        if (androidOptions.AndroidIntentArguments.Count > 0)
        {
            List<object> args = [.. androidOptions.AndroidIntentArguments];

            firefoxOptions["androidIntentArguments"] = args;
        }
    }
}
