// <copyright file="ProxySettingTest.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 BenderProxy;
using BenderProxy.Writers;
using NUnit.Framework;
using OpenQA.Selenium.Environment;
using OpenQA.Selenium.IE;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace OpenQA.Selenium;

[Ignore("Proxy Tests are not working")]
[TestFixture]
public class ProxySettingTest : DriverTestFixture
{
    private IWebDriver localDriver;
    private ProxyServer proxyServer;

    [SetUp]
    public void RestartOriginalDriver()
    {
        driver = EnvironmentManager.Instance.GetCurrentDriver();
        proxyServer = new ProxyServer();
        EnvironmentManager.Instance.DriverStarting += EnvironmentManagerDriverStarting;
    }

    [TearDown]
    public void QuitAdditionalDriver()
    {
        if (localDriver != null)
        {
            localDriver.Quit();
            localDriver = null;
        }

        if (proxyServer != null)
        {
            proxyServer.Quit();
            proxyServer = null;
        }

        EnvironmentManager.Instance.DriverStarting -= EnvironmentManagerDriverStarting;
    }

    [Test]
    [IgnoreBrowser(Browser.Safari, "SafariDriver does not support setting proxy")]
    public void CanConfigureManualHttpProxy()
    {
        proxyServer.EnableLogResourcesOnResponse();
        Proxy proxyToUse = proxyServer.AsProxy();
        InitLocalDriver(proxyToUse);

        localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("simpleTest.html");
        Assert.That(proxyServer.HasBeenCalled("simpleTest.html"), Is.True);
    }

    [Test]
    [IgnoreBrowser(Browser.Safari, "SafariDriver does not support setting proxy")]
    public void CanConfigureNoProxy()
    {
        proxyServer.EnableLogResourcesOnResponse();
        Proxy proxyToUse = proxyServer.AsProxy();
        proxyToUse.AddBypassAddresses(EnvironmentManager.Instance.UrlBuilder.HostName);

        if (TestUtilities.IsInternetExplorer(driver))
        {
            proxyToUse.AddBypassAddress("<-localhost>");
        }

        InitLocalDriver(proxyToUse);

        localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("simpleTest.html");
        Assert.That(proxyServer.HasBeenCalled("simpleTest.html"), Is.False);

        localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIsViaNonLoopbackAddress("simpleTest.html");
        Assert.That(proxyServer.HasBeenCalled("simpleTest.html"), Is.True);
    }

    [Test]
    [IgnoreBrowser(Browser.Safari, "SafariDriver does not support setting proxy")]
    public void CanConfigureProxyThroughAutoConfigFile()
    {
        StringBuilder pacFileContentBuilder = new StringBuilder();
        pacFileContentBuilder.AppendLine("function FindProxyForURL(url, host) {");
        pacFileContentBuilder.AppendFormat("  return 'PROXY {0}';\n", proxyServer.BaseUrl);
        pacFileContentBuilder.AppendLine("}");
        string pacFileContent = pacFileContentBuilder.ToString();

        using (ProxyAutoConfigServer pacServer = new ProxyAutoConfigServer(pacFileContent))
        {
            proxyServer.EnableContentOverwriteOnRequest();
            Proxy proxyToUse = new Proxy();
            proxyToUse.ProxyAutoConfigUrl = string.Format("http://{0}:{1}/proxy.pac", pacServer.HostName, pacServer.Port);
            InitLocalDriver(proxyToUse);
            localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("simpleTest.html");
            Assert.That(localDriver.FindElement(By.TagName("h3")).Text, Is.EqualTo("Hello, world!"));
        }
    }

    [Test]
    [IgnoreBrowser(Browser.Chrome, "ChromeDriver is hanging")]
    [IgnoreBrowser(Browser.Safari, "SafariDriver does not support setting proxy")]
    public void CanUseAutoConfigFileThatOnlyProxiesCertainHosts()
    {
        StringBuilder pacFileContentBuilder = new StringBuilder();
        pacFileContentBuilder.AppendLine("function FindProxyForURL(url, host) {");
        pacFileContentBuilder.AppendFormat("  if (url.indexOf('{0}') != -1) {{\n", EnvironmentManager.Instance.UrlBuilder.HostName);
        pacFileContentBuilder.AppendFormat("    return 'PROXY {0}';\n", proxyServer.BaseUrl);
        pacFileContentBuilder.AppendLine("  }");
        pacFileContentBuilder.AppendLine("  return 'DIRECT';");
        pacFileContentBuilder.AppendLine("}");
        string pacFileContent = pacFileContentBuilder.ToString();

        using (ProxyAutoConfigServer pacServer = new ProxyAutoConfigServer(pacFileContent))
        {
            proxyServer.EnableContentOverwriteOnRequest();
            Proxy proxyToUse = new Proxy();
            proxyToUse.ProxyAutoConfigUrl = string.Format("http://{0}:{1}/proxy.pac", pacServer.HostName, pacServer.Port);
            InitLocalDriver(proxyToUse);
            localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIs("simpleTest.html");
            Assert.That(localDriver.FindElement(By.TagName("h3")).Text, Is.EqualTo("Hello, world!"));
            localDriver.Url = EnvironmentManager.Instance.UrlBuilder.WhereIsViaNonLoopbackAddress("simpleTest.html");
            Assert.That(localDriver.FindElement(By.TagName("h1")).Text, Is.EqualTo("Heading"));
        }
    }

    private void EnvironmentManagerDriverStarting(object sender, DriverStartingEventArgs e)
    {
        InternetExplorerOptions ieOptions = e.Options as InternetExplorerOptions;
        if (ieOptions != null)
        {
            ieOptions.EnsureCleanSession = true;
        }
    }

    private void InitLocalDriver(Proxy proxy)
    {
        EnvironmentManager.Instance.CloseCurrentDriver();
        if (localDriver != null)
        {
            localDriver.Quit();
        }

        ProxyOptions options = new ProxyOptions();
        options.Proxy = proxy;
        localDriver = EnvironmentManager.Instance.CreateDriverInstance(options);
    }

    private class ProxyOptions : DriverOptions
    {
        public override void AddAdditionalOption(string capabilityName, object capabilityValue)
        {
        }

        public override ICapabilities ToCapabilities()
        {
            return null;
        }
    }

    private class ProxyAutoConfigServer : IDisposable
    {
        private int listenerPort;
        private string hostName;
        private string pacFileContent;
        private bool disposedValue = false; // To detect redundant calls
        private bool keepRunning = true;
        private HttpListener listener;
        private Thread listenerThread;

        public ProxyAutoConfigServer(string pacFileContent)
            : this(pacFileContent, "localhost")
        {
        }

        public ProxyAutoConfigServer(string pacFileContent, string hostName)
        {
            this.pacFileContent = pacFileContent;
            this.hostName = hostName;

            //get an empty port
            TcpListener l = new TcpListener(IPAddress.Loopback, 0);
            l.Start();
            this.listenerPort = ((IPEndPoint)l.LocalEndpoint).Port;
            l.Stop();

            this.listenerThread = new Thread(this.Listen);
            this.listenerThread.Start();
        }

        public string HostName
        {
            get { return hostName; }
        }

        public int Port
        {
            get { return listenerPort; }
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    this.keepRunning = false;
                    this.listenerThread.Join(TimeSpan.FromSeconds(5));
                    this.listener.Stop();
                }

                disposedValue = true;
            }
        }

        private void ProcessContext(HttpListenerContext context)
        {
            if (context.Request.Url.AbsoluteUri.ToLowerInvariant().Contains("proxy.pac"))
            {
                byte[] pacFileBuffer = Encoding.ASCII.GetBytes(this.pacFileContent);
                context.Response.ContentType = "application/x-javascript-config";
                context.Response.ContentLength64 = pacFileBuffer.LongLength;
                context.Response.ContentEncoding = Encoding.ASCII;
                context.Response.StatusCode = 200;
                context.Response.OutputStream.Write(pacFileBuffer, 0, pacFileBuffer.Length);
                context.Response.OutputStream.Flush();
            }
        }

        private void Listen()
        {
            listener = new HttpListener();
            listener.Prefixes.Add("http://" + this.HostName + ":" + this.listenerPort.ToString() + "/");
            listener.Start();
            while (this.keepRunning)
            {
                try
                {
                    HttpListenerContext context = listener.GetContext();
                    this.ProcessContext(context);
                }
                catch (HttpListenerException)
                {
                }
            }
        }
    }

    private class ProxyServer
    {
        private HttpProxyServer server;
        private List<string> uris = new List<string>();
        int port;
        string hostName = string.Empty;

        public ProxyServer()
            : this("127.0.0.1")
        {

        }

        public ProxyServer(string hostName)
        {
            this.hostName = hostName;
            this.server = new HttpProxyServer(this.hostName, new HttpProxy());
            this.server.Start().WaitOne();
            this.port = this.server.ProxyEndPoint.Port;
            // this.server.Log += OnServerLog;
        }

        public string BaseUrl
        {
            get { return string.Format("{0}:{1}", this.hostName, this.port); }
        }

        public HttpProxyServer Server
        {
            get { return this.server; }
        }

        public string HostName
        {
            get { return hostName; }
        }

        public int Port
        {
            get { return port; }
        }

        public void EnableLogResourcesOnResponse()
        {
            this.server.Proxy.OnResponseSent = this.LogRequestedResources;
        }

        public void DisableLogResourcesOnResponse()
        {
            this.server.Proxy.OnResponseSent = null;
        }

        public void EnableContentOverwriteOnRequest()
        {
            this.server.Proxy.OnRequestReceived = this.OverwriteRequestedContent;
        }

        public void DisableContentOverwriteOnRequest()
        {
            this.server.Proxy.OnRequestReceived = null;
        }

        public bool HasBeenCalled(string resourceName)
        {
            return this.uris.Contains(resourceName);
        }

        public void Quit()
        {
            this.server.Proxy.OnResponseSent = null;
            this.server.Stop();
        }

        public Proxy AsProxy()
        {
            Proxy proxy = new Proxy();
            proxy.HttpProxy = this.BaseUrl;
            return proxy;
        }

        private void OnServerLog(object sender, BenderProxy.Logging.LogEventArgs e)
        {
            Console.WriteLine(e.LogMessage);
        }

        private void LogRequestedResources(ProcessingContext context)
        {
            string[] parts = context.RequestHeader.RequestURI.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length != 0)
            {
                string finalPart = parts[parts.Length - 1];
                uris.Add(finalPart);
            }
        }

        private void OverwriteRequestedContent(ProcessingContext context)
        {
            StringBuilder pageContentBuilder = new StringBuilder("<!DOCTYPE html>");
            pageContentBuilder.AppendLine("<html>");
            pageContentBuilder.AppendLine("<head>");
            pageContentBuilder.AppendLine("  <title>Hello</title>");
            pageContentBuilder.AppendLine("</head>");
            pageContentBuilder.AppendLine("<body>");
            pageContentBuilder.AppendLine("  <h3>Hello, world!</h3>");
            pageContentBuilder.AppendLine("</body>");
            pageContentBuilder.AppendLine("</html>");
            string pageContent = pageContentBuilder.ToString();

            context.StopProcessing();
            MemoryStream responseStream = new MemoryStream(Encoding.UTF8.GetBytes(pageContent));
            var responseHeader = new BenderProxy.Headers.HttpResponseHeader(200, "OK", "1.1");
            responseHeader.EntityHeaders.ContentType = "text/html";
            responseHeader.EntityHeaders.ContentEncoding = "utf-8";
            responseHeader.EntityHeaders.ContentLength = responseStream.Length;
            new HttpResponseWriter(context.ClientStream).Write(responseHeader, responseStream, responseStream.Length);
        }
    }
}
