using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using AspNet.Security.OpenIdConnect.Primitives;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Tasks;

namespace BTCPayServer.Tests
{
    public enum TestDatabases
    {
        Postgres,
        MySQL,
    }

    public class BTCPayServerTester : IDisposable
    {
        private string _Directory;

        public BTCPayServerTester(string scope)
        {
            this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
        }

        public Uri NBXplorerUri
        {
            get; set;
        }

        public Uri LTCNBXplorerUri { get; set; }

        public Uri ServerUri
        {
            get;
            set;
        }

        public string MySQL
        {
            get; set;
        }

        public string Postgres
        {
            get; set;
        }

        IWebHost _Host;
        public int Port
        {
            get; set;
        }

        public TestDatabases TestDatabase
        {
            get; set;
        }

        public bool MockRates { get; set; } = true;

        public void Start()
        {
            if (!Directory.Exists(_Directory))
                Directory.CreateDirectory(_Directory);
            string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
            string chainDirectory = Path.Combine(_Directory, chain);
            if (!Directory.Exists(chainDirectory))
                Directory.CreateDirectory(chainDirectory);


            StringBuilder config = new StringBuilder();
            config.AppendLine($"{chain.ToLowerInvariant()}=1");
            if (InContainer)
            {
                config.AppendLine($"bind=0.0.0.0");
            }
            config.AppendLine($"port={Port}");
            config.AppendLine($"chains=btc,ltc");

            config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
            config.AppendLine($"btc.explorer.cookiefile=0");
            config.AppendLine("allow-admin-registration=1");
            config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
            config.AppendLine($"ltc.explorer.cookiefile=0");
            config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");

            if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
                config.AppendLine($"mysql=" + MySQL);
            else if (!String.IsNullOrEmpty(Postgres))
                config.AppendLine($"postgres=" + Postgres);
            var confPath = Path.Combine(chainDirectory, "settings.config");
            File.WriteAllText(confPath, config.ToString());

            ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
            HttpClient = new HttpClient();
            HttpClient.BaseAddress = ServerUri;
            Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
            var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
            _Host = new WebHostBuilder()
                    .UseConfiguration(conf)
                    .UseContentRoot(FindBTCPayServerDirectory())
                    .ConfigureServices(s =>
                    {
                        s.AddLogging(l =>
                        {
                            l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
                            l.SetMinimumLevel(LogLevel.Information)
                            .AddFilter("Microsoft", LogLevel.Error)
                            .AddFilter("Hangfire", LogLevel.Error)
                            .AddProvider(Logs.LogProvider);
                        });
                    })
                    .UseKestrel()
                    .UseStartup<Startup>()
                    .Build();
            _Host.Start();

            var urls = _Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
            foreach (var url in urls)
            {
                Logs.Tester.LogInformation("Listening on " + url);
            }
            Logs.Tester.LogInformation("Server URI " + ServerUri);

            InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
            StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
            Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));

            if (MockRates)
            {
                var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
                rateProvider.Providers.Clear();

                var coinAverageMock = new MockRateProvider();
                coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "coinaverage",
                    CurrencyPair = CurrencyPair.Parse("BTC_USD"),
                    BidAsk = new BidAsk(5000m)
                });
                coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "coinaverage",
                    CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
                    BidAsk = new BidAsk(4500m)
                });
                coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "coinaverage",
                    CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
                    BidAsk = new BidAsk(0.001m)
                });
                coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "coinaverage",
                    CurrencyPair = CurrencyPair.Parse("LTC_USD"),
                    BidAsk = new BidAsk(500m)
                });
                rateProvider.Providers.Add("coinaverage", coinAverageMock);

                var bitflyerMock = new MockRateProvider();
                bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "bitflyer",
                    CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
                    BidAsk = new BidAsk(700000m)
                });
                rateProvider.Providers.Add("bitflyer", bitflyerMock);

                var quadrigacx = new MockRateProvider();
                quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "quadrigacx",
                    CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
                    BidAsk = new BidAsk(6000m)
                });
                rateProvider.Providers.Add("quadrigacx", quadrigacx);

                var bittrex = new MockRateProvider();
                bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
                {
                    Exchange = "bittrex",
                    CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
                    BidAsk = new BidAsk(0.004m)
                });
                rateProvider.Providers.Add("bittrex", bittrex);
            }

            

            WaitSiteIsOperational().GetAwaiter().GetResult();
        }

        private async Task WaitSiteIsOperational()
        {
            var synching = WaitIsFullySynched();
            var accessingHomepage = WaitCanAccessHomepage();
            await Task.WhenAll(synching, accessingHomepage).ConfigureAwait(false);
        }

        private async Task WaitCanAccessHomepage()
        {
            var resp = await HttpClient.GetAsync("/").ConfigureAwait(false);
            while (resp.StatusCode != HttpStatusCode.OK)
            {
                await Task.Delay(10).ConfigureAwait(false);
            }
        }

        private async Task WaitIsFullySynched()
        {
            var dashBoard = GetService<NBXplorerDashboard>();
            while (!dashBoard.IsFullySynched())
            {
                await Task.Delay(10).ConfigureAwait(false);
            }
        }

        private string FindBTCPayServerDirectory()
        {
            var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
            return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
        }

        public HttpClient HttpClient { get; set; }

        public string HostName
        {
            get;
            internal set;
        }
        public InvoiceRepository InvoiceRepository { get; private set; }
        public StoreRepository StoreRepository { get; private set; }
        public BTCPayNetworkProvider Networks { get; private set; }
        public Uri IntegratedLightning { get; internal set; }
        public bool InContainer { get; internal set; }

        public T GetService<T>()
        {
            return _Host.Services.GetRequiredService<T>();
        }

        public IServiceProvider ServiceProvider => _Host.Services; 

        public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
        {
            var context = new DefaultHttpContext();
            context.Request.Host = new HostString("127.0.0.1", Port);
            context.Request.Scheme = "http";
            context.Request.Protocol = "http";
            if (userId != null)
            {
                List<Claim> claims = new List<Claim>();
                claims.Add(new Claim(OpenIdConnectConstants.Claims.Subject, userId));
                if (additionalClaims != null)
                    claims.AddRange(additionalClaims);
                context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
            }
            if (storeId != null)
            {
                context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
            }
            var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
            var provider = scope.CreateScope().ServiceProvider;
            context.RequestServices = provider;

            var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
            httpAccessor.HttpContext = context;

            var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));

            controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
            controller.ControllerContext = new ControllerContext()
            {
                HttpContext = context
            };
            return controller;
        }

        public void Dispose()
        {
            if (_Host != null)
                _Host.Dispose();
        }
    }
}
