// <copyright file="EnvironmentManager.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.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using Bazel;

namespace OpenQA.Selenium.Tests.Infrastructure.Environment;

public class EnvironmentManager
{
    private static EnvironmentManager instance;
    private readonly Type driverType;
    private IWebDriver driver;
    private readonly DriverFactory driverFactory;

    private EnvironmentManager()
    {
        string dataFilePath;
        Runfiles runfiles = null;
        try
        {
            runfiles = Runfiles.Create();
            dataFilePath = runfiles.Rlocation("_main/dotnet/test/webdriver/appconfig.json");
        }
        catch (FileNotFoundException)
        {
            dataFilePath = "appconfig.json";
        }
        string currentDirectory = this.CurrentDirectory;

        string content = File.ReadAllText(dataFilePath);
        TestEnvironment env = JsonSerializer.Deserialize<TestEnvironment>(content, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        string activeDriverConfig = System.Environment.GetEnvironmentVariable("ACTIVE_DRIVER_CONFIG") ?? TestContext.Parameters.Get("ActiveDriverConfig", env.ActiveDriverConfig);
        string driverServiceLocation = System.Environment.GetEnvironmentVariable("DRIVER_SERVICE_LOCATION") ?? TestContext.Parameters.Get("DriverServiceLocation", env.DriverServiceLocation);

        string browserLocation = System.Environment.GetEnvironmentVariable("BROWSER_LOCATION") ?? TestContext.Parameters.Get("BrowserLocation", string.Empty);

        // Bazel's $(location ...) expands to a runfiles-relative path (e.g.
        // "../+pin_browsers_extension+mac_chrome/Chrome.app/Contents/MacOS/Chrome")
        // which is relative to <runfiles>/_main. The driver process runs from a
        // different cwd and cannot resolve that, so absolutize through TEST_SRCDIR.
        driverServiceLocation = ResolveRunfilesPath(driverServiceLocation);
        browserLocation = ResolveRunfilesPath(browserLocation);

        DriverConfig driverConfig = env.DriverConfigs[activeDriverConfig];

        this.driverFactory = new DriverFactory(driverServiceLocation, browserLocation);
        this.driverFactory.DriverStarting += OnDriverStarting;

        // Search for the driver type in the all assemblies,
        // bazel uses unpredictable assembly names to execute tests
        driverType = AppDomain.CurrentDomain.GetAssemblies()
            .AsEnumerable()
            .Reverse()
            .Select(assembly => assembly.GetType(driverConfig.DriverTypeName))
            .FirstOrDefault(t => t != null);

        if (driverType == null)
        {
            throw new ArgumentOutOfRangeException($"Unable to find driver type {driverConfig.DriverTypeName}");
        }

        Browser = driverConfig.BrowserValue;
        RemoteCapabilities = driverConfig.RemoteCapabilities;

        WebServer = new AppServer();
        WebServer.StartAsync().GetAwaiter().GetResult();

        // Find selenium-manager binary.
        try
        {
            string managerFilePath = "";
            runfiles ??= Runfiles.Create();

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/windows/selenium-manager.exe");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/linux/selenium-manager");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/macos/selenium-manager");
            }

            System.Environment.SetEnvironmentVariable("SE_MANAGER_PATH", managerFilePath);
        }
        catch (FileNotFoundException)
        {
            // Use the default one.
        }

        string projectRoot = System.Environment.GetEnvironmentVariable("TEST_SRCDIR");
        if (!string.IsNullOrEmpty(projectRoot))
        {
            projectRoot += "/_main";
        }
        else
        {
            projectRoot = FindProjectRoot(currentDirectory);
        }

        bool autoStartRemoteServer = false;
        if (Browser == Browser.Remote)
        {
            autoStartRemoteServer = driverConfig.AutoStartRemoteServer;
        }

        RemoteServer = new RemoteSeleniumServer(projectRoot, autoStartRemoteServer);
    }

    ~EnvironmentManager()
    {
        RemoteServer?.StopAsync().Wait();
        WebServer?.StopAsync().Wait();
        CloseCurrentDriver();
    }

    public event EventHandler<DriverStartingEventArgs> DriverStarting;

    public static EnvironmentManager Instance => instance ??= new EnvironmentManager();

    public Browser Browser { get; }

    public string CurrentDirectory
    {
        get
        {
            string assemblyLocation = Path.GetDirectoryName(typeof(EnvironmentManager).Assembly.Location);
            string testDirectory = TestContext.CurrentContext.TestDirectory;
            if (assemblyLocation != testDirectory)
            {
                return assemblyLocation;
            }
            return testDirectory;
        }
    }

    public AppServer WebServer { get; }

    public RemoteSeleniumServer RemoteServer { get; }

    public string RemoteCapabilities { get; }

    public IWebDriver GetCurrentDriver()
    {
        return driver ?? CreateFreshDriver();
    }

    public IWebDriver CreateDriverInstance()
    {
        return driverFactory.CreateDriver(driverType);
    }

    public IWebDriver CreateDriverInstance(DriverOptions options)
    {
        return driverFactory.CreateDriverWithOptions(driverType, options);
    }

    public IWebDriver CreateFreshDriver()
    {
        CloseCurrentDriver();
        driver = CreateDriverInstance();
        return driver;
    }

    public void CloseCurrentDriver()
    {
        driver?.Quit();
        driver = null;
    }

    protected void OnDriverStarting(object sender, DriverStartingEventArgs e)
    {
        this.DriverStarting?.Invoke(sender, e);
    }

    private static string FindProjectRoot(string startDirectory)
    {
        // Walk up until we find a directory containing common/ and dotnet/
        DirectoryInfo info = new DirectoryInfo(startDirectory);
        while (info is not null && info != info.Root)
        {
            if (Directory.Exists(Path.Combine(info.FullName, "common"))
                && Directory.Exists(Path.Combine(info.FullName, "dotnet")))
            {
                return info.FullName;
            }
            info = info.Parent;
        }

        return startDirectory;
    }

    private static string ResolveRunfilesPath(string path)
    {
        if (string.IsNullOrEmpty(path) || Path.IsPathRooted(path))
        {
            return path;
        }

        string srcdir = System.Environment.GetEnvironmentVariable("TEST_SRCDIR");
        if (string.IsNullOrEmpty(srcdir))
        {
            return path;
        }

        // Bazel $(location ...) emits paths relative to <runfiles>/_main.
        return Path.GetFullPath(Path.Combine(srcdir, "_main", path));
    }
}
