using System;
using Xunit;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using BTCPayServer.Tests.Logging;
using Xunit.Abstractions;
using OpenQA.Selenium.Interactions;
using System.Linq;
using NBitcoin;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

namespace BTCPayServer.Tests
{
    [Trait("Selenium", "Selenium")]
    public class ChromeTests
    {
        public const int TestTimeout = TestUtils.TestTimeout;
        public ChromeTests(ITestOutputHelper helper)
        {
            Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
            Logs.LogProvider = new XUnitLogProvider(helper);
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanNavigateServerSettings()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.RegisterNewUser(true);
                s.Driver.FindElement(By.Id("ServerSettings")).Click();
                s.Driver.AssertNoError();
                s.ClickOnAllSideMenus();
                s.Driver.Quit();
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task NewUserLogin()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                //Register & Log Out
                var email = s.RegisterNewUser();
                s.Driver.FindElement(By.Id("Logout")).Click();
                s.Driver.AssertNoError();
                s.Driver.FindElement(By.Id("Login")).Click();
                s.Driver.AssertNoError();

                s.Driver.Navigate().GoToUrl(s.Link("/invoices"));
                Assert.Contains("ReturnUrl=%2Finvoices", s.Driver.Url);

                // We should be redirected to login
                //Same User Can Log Back In
                s.Driver.FindElement(By.Id("Email")).SendKeys(email);
                s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
                s.Driver.FindElement(By.Id("LoginButton")).Click();

                // We should be redirected to invoice
                Assert.EndsWith("/invoices", s.Driver.Url);

                // Should not be able to reach server settings
                s.Driver.Navigate().GoToUrl(s.Link("/server/users"));
                Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url);

                //Change Password & Log Out
                s.Driver.FindElement(By.Id("MySettings")).Click();
                s.Driver.FindElement(By.Id("ChangePassword")).Click();
                s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
                s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
                s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
                s.Driver.FindElement(By.Id("UpdatePassword")).Click();
                s.Driver.FindElement(By.Id("Logout")).Click();
                s.Driver.AssertNoError();

                //Log In With New Password
                s.Driver.FindElement(By.Id("Login")).Click();
                s.Driver.FindElement(By.Id("Email")).SendKeys(email);
                s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
                s.Driver.FindElement(By.Id("LoginButton")).Click();
                Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores");

                s.Driver.FindElement(By.Id("MySettings")).Click();
                s.ClickOnAllSideMenus();

                s.Driver.Quit();
            }
        }

        static void LogIn(SeleniumTester s, string email)
        {
            s.Driver.FindElement(By.Id("Login")).Click();
            s.Driver.FindElement(By.Id("Email")).SendKeys(email);
            s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
            s.Driver.FindElement(By.Id("LoginButton")).Click();
            s.Driver.AssertNoError();
        }
        [Fact(Timeout = TestTimeout)]
        public async Task CanUseSSHService()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                var alice = s.RegisterNewUser(isAdmin: true);
                s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
                Assert.Contains("server/services/ssh", s.Driver.PageSource);
                using (var client = await s.Server.PayTester.GetService<BTCPayServer.Configuration.BTCPayServerOptions>().SSHSettings.ConnectAsync())
                {
                    var result = await client.RunBash("echo hello");
                    Assert.Equal(string.Empty, result.Error);
                    Assert.Equal("hello\n", result.Output);
                    Assert.Equal(0, result.ExitStatus);
                }
                s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
                s.Driver.AssertNoError();
                s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
                s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2");
                s.Driver.FindElement(By.Id("submit")).ForceClick();
                s.Driver.AssertNoError();

                var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
                // Browser replace \n to \r\n, so it is hard to compare exactly what we want
                Assert.Contains("tes't", text);
                Assert.Contains("test2", text);
                Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase));

                s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
                s.Driver.FindElement(By.Id("submit")).ForceClick();

                text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
                Assert.DoesNotContain("test2", text);
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanUseDynamicDns()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                var alice = s.RegisterNewUser(isAdmin: true);
                s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
                Assert.Contains("Dynamic DNS", s.Driver.PageSource);

                s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
                s.Driver.AssertNoError();
                if (s.Driver.PageSource.Contains("pouet.hello.com"))
                {
                    // Cleanup old test run
                    s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
                    s.Driver.FindElement(By.Id("continue")).Click();
                }
                s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
                s.Driver.AssertNoError();
                // We will just cheat for test purposes by only querying the server
                s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
                s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com");
                s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog");
                s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter);
                s.Driver.AssertNoError();
                Assert.Contains("The Dynamic DNS has been successfully queried", s.Driver.PageSource);
                Assert.EndsWith("/server/services/dynamic-dns", s.Driver.Url);

                // Try to do the same thing should fail (hostname already exists)
                s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
                s.Driver.AssertNoError();
                s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
                s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com");
                s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog");
                s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter);
                s.Driver.AssertNoError();
                Assert.Contains("This hostname already exists", s.Driver.PageSource);

                // Delete it
                s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
                Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
                s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
                s.Driver.FindElement(By.Id("continue")).Click();
                s.Driver.AssertNoError();

                Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanCreateStores()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                var alice = s.RegisterNewUser();
                var store = s.CreateNewStore().storeName;
                s.AddDerivationScheme();
                s.Driver.AssertNoError();
                Assert.Contains(store, s.Driver.PageSource);
                var storeUrl = s.Driver.Url;
                s.ClickOnAllSideMenus();
                s.GoToInvoices();
                s.CreateInvoice(store);
                s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
                var invoiceUrl = s.Driver.Url;

                // When logout we should not be able to access store and invoice details
                s.Driver.FindElement(By.Id("Logout")).Click();
                s.Driver.Navigate().GoToUrl(storeUrl);
                Assert.Contains("ReturnUrl", s.Driver.Url);
                s.Driver.Navigate().GoToUrl(invoiceUrl);
                Assert.Contains("ReturnUrl", s.Driver.Url);

                // When logged we should not be able to access store and invoice details
                var bob = s.RegisterNewUser();
                s.Driver.Navigate().GoToUrl(storeUrl);
                Assert.Contains("ReturnUrl", s.Driver.Url);
                s.Driver.Navigate().GoToUrl(invoiceUrl);
                s.AssertNotFound();
                s.GoToHome();
                s.Logout();

                // Let's add Bob as a guest to alice's store
                LogIn(s, alice);
                s.Driver.Navigate().GoToUrl(storeUrl + "/users");
                s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
                Assert.Contains("User added successfully", s.Driver.PageSource);
                s.Logout();

                // Bob should not have access to store, but should have access to invoice
                LogIn(s, bob);
                s.Driver.Navigate().GoToUrl(storeUrl);
                Assert.Contains("ReturnUrl", s.Driver.Url);
                s.Driver.Navigate().GoToUrl(invoiceUrl);
                s.Driver.AssertNoError();

                // Alice should be able to delete the store
                s.Logout();
                LogIn(s, alice);
                s.Driver.FindElement(By.Id("Stores")).Click();
                s.Driver.FindElement(By.LinkText("Remove")).Click();
                s.Driver.FindElement(By.Id("continue")).Click();
                s.Driver.FindElement(By.Id("Stores")).Click();
                s.Driver.Navigate().GoToUrl(storeUrl);
                Assert.Contains("ReturnUrl", s.Driver.Url);
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanUsePairing()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.Driver.Navigate().GoToUrl(s.Link("/api-access-request"));
                Assert.Contains("ReturnUrl", s.Driver.Url);

                var alice = s.RegisterNewUser();
                var store = s.CreateNewStore().storeName;
                s.AddDerivationScheme();

                s.Driver.FindElement(By.Id("Tokens")).Click();
                s.Driver.FindElement(By.Id("CreateNewToken")).Click();
                s.Driver.FindElement(By.Id("RequestPairing")).Click();

                string pairingCode = AssertUrlHasPairingCode(s);

                s.Driver.FindElement(By.Id("ApprovePairing")).Click();
                Assert.Contains(pairingCode, s.Driver.PageSource);

                var client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri);
                await client.AuthorizeClient(new NBitpayClient.PairingCode(pairingCode));
                await client.CreateInvoiceAsync(new NBitpayClient.Invoice()
                {
                    Price = 0.000000012m,
                    Currency = "USD",
                    FullNotifications = true
                }, NBitpayClient.Facade.Merchant);

                client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri);

                var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant);
                s.Driver.Navigate().GoToUrl(code.CreateLink(s.Server.PayTester.ServerUri));
                s.Driver.FindElement(By.Id("ApprovePairing")).Click();

                await client.CreateInvoiceAsync(new NBitpayClient.Invoice()
                {
                    Price = 0.000000012m,
                    Currency = "USD",
                    FullNotifications = true
                }, NBitpayClient.Facade.Merchant);

                s.Driver.Navigate().GoToUrl(s.Link("/api-tokens"));
                s.Driver.FindElement(By.Id("RequestPairing")).Click();
                s.Driver.FindElement(By.Id("ApprovePairing")).Click();
                AssertUrlHasPairingCode(s);
            }
        }

        private static string AssertUrlHasPairingCode(SeleniumTester s)
        {
            var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)");
            Assert.True(regex.Success, $"{s.Driver.Url} does not match expected regex");
            var pairingCode = regex.Groups[1].Value;
            return pairingCode;
        }


        [Fact(Timeout = TestTimeout)]
        public async Task CanCreateAppPoS()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.RegisterNewUser();
                var store = s.CreateNewStore();

                s.Driver.FindElement(By.Id("Apps")).Click();
                s.Driver.FindElement(By.Id("CreateNewApp")).Click();
                s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + Guid.NewGuid());
                s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter);
                s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
                s.Driver.FindElement(By.Id("Create")).Click();
                s.Driver.FindElement(By.Id("EnableShoppingCart")).Click();
                s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
                s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
                s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
                Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
                s.Driver.Quit();
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanCreateAppCF()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.RegisterNewUser();
                var store = s.CreateNewStore();
                s.AddDerivationScheme();

                s.Driver.FindElement(By.Id("Apps")).Click();
                s.Driver.FindElement(By.Id("CreateNewApp")).Click();
                s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + Guid.NewGuid());
                s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund" + Keys.Enter);
                s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
                s.Driver.FindElement(By.Id("Create")).Click();
                s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
                s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
                s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
                s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
                s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
                s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
                s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
                Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF");
                s.Driver.Quit();
            } 
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanCreatePayRequest()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.RegisterNewUser();
                s.CreateNewStore();
                s.AddDerivationScheme();

                s.Driver.FindElement(By.Id("PaymentRequests")).Click();
                s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
                s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
                s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
                s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
                s.Driver.FindElement(By.Id("SaveButton")).ForceClick();
                s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return);
                s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
                Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request");
                s.Driver.Quit();
            }
        }

        [Fact(Timeout = TestTimeout)]
        public async Task CanManageWallet()
        {
            using (var s = SeleniumTester.Create())
            {
                await s.StartAsync();
                s.RegisterNewUser(true);
                s.CreateNewStore();

                // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed 
                // to sign the transaction
                var mnemonic = "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage";
                var root = new Mnemonic(mnemonic).DeriveExtKey();
                s.AddDerivationScheme("BTC", "ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
                var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
                s.Server.ExplorerNode.Generate(1);

                s.Driver.FindElement(By.Id("Wallets")).Click();
                s.Driver.FindElement(By.LinkText("Manage")).Click();

                s.ClickOnAllSideMenus();

                // Make sure we can rescan, because we are admin!
                s.Driver.FindElement(By.Id("WalletRescan")).ForceClick();
                Assert.Contains("The batch size make sure", s.Driver.PageSource);

                // We setup the fingerprint and the account key path
                s.Driver.FindElement(By.Id("WalletSettings")).ForceClick();
                s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
                s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);

                // Check the tx sent earlier arrived
                s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick();
                var walletTransactionLink = s.Driver.Url;
                Assert.Contains(tx.ToString(), s.Driver.PageSource);

                
                void SignWith(string signingSource)
                {
                    // Send to bob
                    s.Driver.FindElement(By.Id("WalletSend")).Click();
                    var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
                    SetTransactionOutput(0, bob, 1);
                    s.Driver.ScrollTo(By.Id("SendMenu"));
                    s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
                    s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();

                    // Input the seed
                    s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);

                    // Broadcast
                    Assert.Contains(bob.ToString(), s.Driver.PageSource);
                    Assert.Contains("1.00000000", s.Driver.PageSource);
                    s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
                    Assert.Equal(walletTransactionLink, s.Driver.Url);
                }

                void SetTransactionOutput(int index, BitcoinAddress dest, decimal amount, bool subtract = false)
                {
                    s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
                    var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
                    amountElement.Clear();
                    amountElement.SendKeys(amount.ToString());
                    var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
                    if (checkboxElement.Selected != subtract)
                    {
                        checkboxElement.Click();
                    }
                }
                SignWith(mnemonic);
                var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
                SignWith(accountKey);
            }
        }
    }
}
