using System;
using System.IO;
using System.Net;
using NUnit.Framework;
using UnityEditor.AddressableAssets.HostingServices;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using UnityEngine.TestTools;

namespace UnityEditor.AddressableAssets.Tests.HostingServices
{
    using Random = System.Random;

    public class HttpHostingServiceTests
    {
        class MyWebClient : WebClient
        {
            protected override WebRequest GetWebRequest(Uri uri)
            {
                var w = base.GetWebRequest(uri);
                Debug.Assert(w != null);
                w.Timeout = 2000;
                return w;
            }
        }

        HttpHostingService m_Service;
        string m_ContentRoot;
        readonly WebClient m_Client;

        public HttpHostingServiceTests()
        {
            m_Client = new MyWebClient();
        }

        static byte[] GetRandomBytes(int size)
        {
            var rand = new Random();
            var buf = new byte[size];
            rand.NextBytes(buf);
            return buf;
        }

        [OneTimeSetUp]
        public void SetUp()
        {
            m_Service = new HttpHostingService();
            var dirName = Path.GetRandomFileName();
            m_ContentRoot = Path.Combine(Path.GetTempPath(), dirName);
            Assert.IsNotEmpty(m_ContentRoot);
            Directory.CreateDirectory(m_ContentRoot);
            m_Service.HostingServiceContentRoots.Add(m_ContentRoot);
        }

        [TearDown]
        public void Cleanup()
        {
            m_Service.StopHostingService();
        }

        [OneTimeTearDown]
        public void TearDown()
        {
            if (!string.IsNullOrEmpty(m_ContentRoot) && Directory.Exists(m_ContentRoot))
                Directory.Delete(m_ContentRoot, true);
        }

        [TestCase("subdir", "subdir1")] //"subdir3")]
        [TestCase("subdír☠", "subdirãúñ", TestName = "ShouldServeFilesWSpecialCharacters")] //"subdirü",
        public void ShouldServeRequestedFiles(string subdir1, string subdir2) // string subdir3)
        {
            var fileNames = new[]
            {
                Path.GetRandomFileName(),
                Path.Combine(subdir1, Path.GetRandomFileName()),
                Path.Combine(subdir2, Path.GetRandomFileName())
            };

            foreach (var fileName in fileNames)
            {
                var filePath = Path.Combine(m_ContentRoot, fileName);
                var bytes = GetRandomBytes(1024);
                Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                File.WriteAllBytes(filePath, bytes);
                m_Service.StartHostingService();
                Assert.IsTrue(m_Service.IsHostingServiceRunning);
                var url = string.Format("http://127.0.0.1:{0}/{1}", m_Service.HostingServicePort, fileName);
                try
                {
                    var data = m_Client.DownloadData(url);
                    Assert.AreEqual(data.Length, bytes.Length);
                    for (var i = 0; i < data.Length; i++)
                        if (bytes[i] != data[i])
                            Assert.Fail("Data does not match {0} != {1}", bytes[i], data[i]);
                }
                catch (Exception e)
                {
                    Assert.Fail(e.Message);
                }
            }
        }

        private class MockHttpContext : HttpHostingService.IHttpContext
        {
            public Uri Url { get; set; }
            public string ContentType { get; set; }
            public long ContentLength { get; set; }

            public Stream OutputStream { get; set; }

            public Uri GetRequestUrl()
            {
                return Url;
            }

            public void SetResponseContentType(string contentType)
            {
                ContentType = contentType;
            }

            public void SetResponseContentLength(long contentLength)
            {
                ContentLength = contentLength;
            }

            public Stream GetResponseOutputStream()
            {
                return OutputStream;
            }
        }

        [Test]
        public void FileUploadOperationSplitsDownload()
        {

            string subdir1 = "subdir";
            string subdir2 = "subdir1"; // Remove comment when Mono limit Fixed
            string subdir3 = "subdir3";

            var fileNames = new[]
            {
                Path.GetRandomFileName(),
                Path.Combine(subdir1, Path.GetRandomFileName()),
                Path.Combine(subdir2, Path.Combine(subdir3, Path.GetRandomFileName())),
                Path.Combine(subdir3, Path.GetRandomFileName()),
            };

            foreach (var fileName in fileNames)
            {
                var filePath = Path.Combine(m_ContentRoot, fileName);
                var bytes = GetRandomBytes(1024);
                Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                File.WriteAllBytes(filePath, bytes);
                // we execute every 250ms so you can divide upload speed by 4
                // to see how much each update will process
                var uploadSpeed = 2048;

                var cleanupCalled = false;
                Action cleanup = () => { cleanupCalled = true; };
                var context = new MockHttpContext();
                using (MemoryStream outputStream = new MemoryStream(1024))
                {
                    context.OutputStream = outputStream;
                    context.Url = new Uri("http://127.0.0.1:55555");
                    var op = new HttpHostingService.FileUploadOperation(context, filePath, uploadSpeed, cleanup);
                    op.Update(null);
                    Assert.AreEqual(512, context.OutputStream.Length);
                    op.Update(null);
                }
                Assert.AreEqual(1024, context.ContentLength);
                Assert.AreEqual("application/octet-stream", context.ContentType);
                Assert.IsTrue(cleanupCalled);
            }
        }

        [Test]
        public void FileUploadOperationCallsCleanupOnError()
        {

            string subdir1 = "subdir";
            var fileName = Path.Combine(subdir1, Path.GetRandomFileName());
            var filePath = Path.Combine(m_ContentRoot, fileName);
            // we intentionally do not initialize a test file
            var uploadSpeed = 2048;

            var exceptionThrown = false;
            var cleanupCalled = false;
            Action cleanup = () => { cleanupCalled = true; };
            var context = new MockHttpContext();
            try
            {
                var _ = new HttpHostingService.FileUploadOperation(context, filePath, uploadSpeed, cleanup);
            }
            catch (Exception e)
            {
                exceptionThrown = true;
            }

            LogAssert.Expect(LogType.Exception, $"DirectoryNotFoundException: Could not find a part of the path \"{filePath}\".");
            Assert.IsTrue(cleanupCalled);
            Assert.IsTrue(exceptionThrown);
        }

        [Test]
        public void FileUploadOperationHandlesError()
        {

            var fileName = Path.GetRandomFileName();

            var filePath = Path.Combine(m_ContentRoot, fileName);
            var bytes = GetRandomBytes(1024);
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            File.WriteAllBytes(filePath, bytes);
            // we execute every 250ms so you can divide upload speed by 4
            // to see how much each update will process
            var uploadSpeed = 2048;

            var cleanupCalled = false;
            var exceptionCaught = false;
            Action cleanup = () => { cleanupCalled = true; };
            var context = new MockHttpContext();
            using (MemoryStream outputStream = new MemoryStream(1024))
            {
                try
                {
                    context.OutputStream = outputStream;
                    // close the output stream to trigger an exception on writes
                    outputStream.Close();
                    context.Url = new Uri("http://127.0.0.1:55555");
                    var op = new HttpHostingService.FileUploadOperation(context, filePath, uploadSpeed, cleanup);
                    op.Update(null);
                }
                catch (Exception e)
                {
                    exceptionCaught = true;
                }
            }
            Assert.IsTrue(cleanupCalled);
            Assert.IsTrue(exceptionCaught);
        }


        [Test]
        public void HttpServiceCompletesWithUploadSpeedWhenExpected()
        {
            string subdir1 = "subdir";
            string subdir2 = "subdir1";
            string subdir3 = "subdir3";

            var fileNames = new[]
            {
                Path.GetRandomFileName(),
                Path.Combine(subdir1, Path.GetRandomFileName()),
                Path.Combine(subdir2, Path.Combine(subdir3, Path.GetRandomFileName())),
                Path.Combine(subdir3, Path.GetRandomFileName()),
            };

            m_Service.StartHostingService();
            try
            {
                foreach (var fileName in fileNames)
                {
                    var filePath = Path.Combine(m_ContentRoot, fileName);
                    var bytes = GetRandomBytes(1024);
                    Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                    File.WriteAllBytes(filePath, bytes);

                    m_Service.UploadSpeed = 2048;

                    Assert.IsTrue(m_Service.IsHostingServiceRunning);
                    var url = string.Format("http://127.0.0.1:{0}/{1}", m_Service.HostingServicePort, fileName);
                    try
                    {
                        var downloadedBytes = m_Client.DownloadData(new Uri(url));
                        Assert.AreEqual(1024, downloadedBytes.Length);
                    }
                    catch (Exception e)
                    {
                        Assert.Fail(e.Message);
                    }
                }
            }
            finally
            {
                m_Service.StopHostingService();
            }
        }

        [Test]
        public void ShouldRespondWithStatus404IfFileDoesNotExist()
        {
            m_Service.StartHostingService();
            Assert.IsTrue(m_Service.IsHostingServiceRunning);
            var url = string.Format("http://127.0.0.1:{0}/{1}", m_Service.HostingServicePort, "foo");
            try
            {
                m_Client.DownloadData(url);
            }
            catch (WebException e)
            {
                var response = (HttpWebResponse)e.Response;
                Assert.AreEqual(response.StatusCode, HttpStatusCode.NotFound);
            }
            catch (Exception)
            {
                Assert.Fail();
            }
        }

        // StartHostingService

        [Test]
        public void StartHostingServiceShould_AssignPortIfUnassigned()
        {
            m_Service.StartHostingService();
            Assert.Greater(m_Service.HostingServicePort, 0);
        }

        // OnBeforeSerialize

        [Test]
        public void OnBeforeSerializeShould_PersistExpectedDataToKeyDataStore()
        {
            m_Service.StartHostingService();
            var port = m_Service.HostingServicePort;
            var data = new KeyDataStore();
            m_Service.OnBeforeSerialize(data);
            Assert.AreEqual(port, data.GetData("HostingServicePort", 0));
        }

        [Test]
        public void OnBeforeSerializeShould_WasEnableCorrectToKeyDataStore()
        {
            m_Service.StartHostingService();
            var data = new KeyDataStore();
            m_Service.OnDisable();
            m_Service.OnBeforeSerialize(data);
            Assert.IsTrue(data.GetData("IsEnabled", false), "Hosting server was started before shutting down. IsEnabled expected to be true");
        }

        // OnAfterDeserialize

        [Test]
        public void OnAfterDeserializeShould_RestoreExpectedDataFromKeyDataStore()
        {
            var data = new KeyDataStore();
            data.SetData("HostingServicePort", 1234);
            m_Service.OnAfterDeserialize(data);
            Assert.AreEqual(1234, m_Service.HostingServicePort);
        }

        // ResetListenPort

        [Test]
        public void ResetListenPortShould_AssignTheGivenPort()
        {
            m_Service.ResetListenPort(1234);
            Assert.AreEqual(1234, m_Service.HostingServicePort);
        }

        [Test]
        public void ResetListenPortShould_AssignRandomPortIfZero()
        {
            var oldPort = m_Service.HostingServicePort;
            m_Service.ResetListenPort();
            m_Service.StartHostingService();
            Assert.Greater(m_Service.HostingServicePort, 0);
            Assert.AreNotEqual(m_Service.HostingServicePort, oldPort);
        }

        [Test]
        public void ResetListenPortShouldNot_StartServiceIfItIsNotRunning()
        {
            m_Service.StopHostingService();
            m_Service.ResetListenPort();
            Assert.IsFalse(m_Service.IsHostingServiceRunning);
        }

        [Test]
        public void ResetListenPortShould_RestartServiceIfRunning()
        {
            m_Service.StartHostingService();
            m_Service.ResetListenPort();
            Assert.IsTrue(m_Service.IsHostingServiceRunning);
        }
    }
}