using System.Collections;
using System.Diagnostics;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting;
using Managing.Application.Bots.Base;
using Managing.Core;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Moq;
using Newtonsoft.Json;
using Xunit;
using static Managing.Common.Enums;

namespace Managing.Application.Tests
{
    public class BotsTests : BaseTests
    {
        private readonly IBotFactory _botFactory;
        private readonly IBacktester _backtester;
        private readonly string _reportPath = "D:\\BacktestingReports\\backtesting.csv";
        private string _analysePath = "D:\\BacktestingReports\\analyse";
        private readonly string _errorsPath = "D:\\BacktestingReports\\errorsAnalyse.csv";
        private readonly string _s = "|";
        private List<double> _elapsedTimes { get; set; }

        public BotsTests() : base()
        {
            var backtestRepository = new Mock<IBacktestRepository>().Object;
            var discordService = new Mock<IMessengerService>().Object;
            var scenarioService = new Mock<IScenarioService>().Object;
            var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
            var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
            var botService = new Mock<IBotService>().Object;
            _botFactory = new BotFactory(
                _exchangeService,
                tradingBotLogger,
                discordService,
                _accountService.Object,
                _tradingService.Object,
                botService);
            _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
                scenarioService);
            _elapsedTimes = new List<double>();
        }

        [Theory]
        [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -10)]
        public async Task SwingBot_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe,
            int days)
        {
            // Arrange 
            var scenario = new Scenario("FlippingScenario");
            var strategy = ScenarioHelpers.BuildStrategy(StrategyType.RsiDivergence, "RsiDiv", period: 14);
            scenario.AddStrategy(strategy);
            var localCandles =
                FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");

            // Act
            var backtestResult = await _backtester.RunFlippingBotBacktest(_account, MoneyManagement, ticker, scenario,
                timeframe, 1000, new DateTime().AddDays(-3), DateTime.UtcNow,
                initialCandles: localCandles.TakeLast(500).ToList());

            var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
            File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json);
            // WriteCsvReport(backtestResult.GetStringReport());

            // Assert
            Assert.True(backtestResult.FinalPnl > 0);
            Assert.True(backtestResult.WinRate >= 30);
            Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage);
        }

        [Theory]
        //[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.ThirtyMinutes, -5)]
        //[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.FifteenMinutes, -5)]
        //[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.ThirtyMinutes, -4)]
        //[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.FifteenMinutes, -4)]
        //[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.ThirtyMinutes, -4)]
        //[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.FifteenMinutes, -4)]
        [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -14)]
        public async Task ScalpingBot_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker,
            Timeframe timeframe,
            int days)
        {
            // Arrange 
            var scenario = new Scenario("ScalpingScenario");
            var strategy = ScenarioHelpers.BuildStrategy(StrategyType.RsiDivergence, "RsiDiv", period: 5);
            scenario.AddStrategy(strategy);

            // Act
            var backtestResult = await _backtester.RunScalpingBotBacktest(_account, MoneyManagement, ticker, scenario,
                timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
            //WriteCsvReport(backtestResult.GetStringReport());

            // Assert
            Assert.True(backtestResult.FinalPnl > 0);
            Assert.True(backtestResult.WinRate >= 30);
            Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage);
        }

        [Theory]
        [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -8)]
        public async Task MacdCross_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe,
            int days)
        {
            // Arrange 
            var scenario = new Scenario("ScalpingScenario");
            var strategy = ScenarioHelpers.BuildStrategy(StrategyType.MacdCross, "RsiDiv", fastPeriods: 12,
                slowPeriods: 26, signalPeriods: 9);
            scenario.AddStrategy(strategy);

            var moneyManagement = new MoneyManagement()
            {
                Leverage = 1,
                Timeframe = timeframe,
                StopLoss = 0.01m,
                TakeProfit = 0.02m
            };

            // Act
            var backtestResult = await _backtester.RunScalpingBotBacktest(_account, moneyManagement, ticker, scenario,
                timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
            WriteCsvReport(backtestResult.GetStringReport());

            // Assert
            Assert.True(backtestResult.FinalPnl > 0);
            Assert.True(backtestResult.WinRate >= 30);
            Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage);
        }

        [Theory]
        [InlineData(Timeframe.FifteenMinutes, -6, StrategyType.Stc, BotType.ScalpingBot)]
        //[InlineData(Timeframe.FifteenMinutes, -6, Enums.StrategyType.RsiDivergenceConfirm, Enums.BotType.FlippingBot)]
        public void GetBestPeriodRsiForDivergenceFlippingBot(Timeframe timeframe, int days, StrategyType strategyType,
            BotType botType)
        {
            var result = new List<Tuple<string, int, decimal, decimal, decimal, decimal>>();
            var errors = new List<string>();
            var options = new ParallelOptions()
            {
                MaxDegreeOfParallelism = 4
            };

            var periodRange = new List<int>() { 2, 7 };
            var stopLossRange = new List<decimal>() { 0.005m, 0.05m, 0.005m };
            var takeProfitRange = new List<decimal>() { 0.01m, 0.1m, 0.02m };
            var fileIdentifier = $"{strategyType}-{timeframe}";
            var completedTest = 0;
            var totalTests = GetTotalTrades(periodRange, stopLossRange, takeProfitRange) *
                             Enum.GetNames(typeof(Ticker)).Length;

            CleanAnalyseFile(fileIdentifier);
            UpdateProgression(totalTests, completedTest);

            Parallel.ForEach((Ticker[])Enum.GetValues(typeof(Ticker)), options, ticker =>
            {
                var candles = _exchangeService
                    .GetCandles(_account, ticker, DateTime.Now.AddDays(Convert.ToDouble(days)), timeframe, true).Result;

                if (candles == null || candles.Count == 0)
                    return;

                Parallel.For(periodRange[0], periodRange[1], options, i =>
                {
                    var scenario = new Scenario("ScalpingScenario");
                    var strategy = ScenarioHelpers.BuildStrategy(strategyType, "RsiDiv", period: i);
                    scenario.AddStrategy(strategy);

                    // -0.5 to -5
                    for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
                    {
                        // +1% to +10% in 1%
                        for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2])
                        {
                            var moneyManagement = new MoneyManagement()
                            {
                                Leverage = 1,
                                Timeframe = timeframe,
                                StopLoss = s,
                                TakeProfit = t
                            };

                            try
                            {
                                var timer = new Stopwatch();
                                timer.Start();

                                var backtestResult = botType switch
                                {
                                    BotType.SimpleBot => throw new NotImplementedException(),
                                    BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(_account, moneyManagement,
                                        scenario, timeframe, candles, 1000, null).Result,
                                    BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
                                        scenario, timeframe, candles, 1000, null).Result,
                                    _ => throw new NotImplementedException(),
                                };
                                timer.Stop();

                                if (backtestResult.FinalPnl > 0
                                    && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
                                    && backtestResult.Statistics.MaxDrawdown < 3)
                                {
                                    var currentResult = new Tuple<string, int, decimal, decimal, decimal, decimal>(
                                        ticker.ToString(), i,
                                        backtestResult.FinalPnl, s, t,
                                        backtestResult.GrowthPercentage - backtestResult.HodlPercentage);
                                    result.Add(currentResult);
                                }

                                completedTest++;
                                UpdateProgression(totalTests, completedTest, timer.Elapsed.TotalSeconds);
                            }
                            catch (Exception ex)
                            {
                                completedTest++;
                                errors.Add($"{ticker}{_s}{i}{_s}{s}{_s}{t}{_s}{ex.Message}");
                            }
                        }
                    }
                });
            });

            foreach (var r in result)
            {
                WriteCsvAnalyse(
                    $"{r.Item1}{_s}{r.Item2}{_s}{r.Item3.ToString("0.000")}{_s}{r.Item4 * 100}{_s}{r.Item5 * 100}{_s}{r.Item6}");
            }

            foreach (var e in errors)
            {
                WriteCsvErrors(e);
            }

            var bestResult = result.OrderByDescending(b => b.Item3).FirstOrDefault();
            WriteCsvAnalyse(
                $"Best result : {bestResult.Item1} - Rsi Period : {bestResult.Item2} - {bestResult.Item3} - SL : {bestResult.Item4}% - TP : {bestResult.Item5}%");

            Assert.True(result.Any());
        }


        [Theory]
        [InlineData(Timeframe.OneHour, -30, StrategyType.MacdCross, BotType.FlippingBot)]
        [InlineData(Timeframe.OneHour, -30, StrategyType.MacdCross, BotType.ScalpingBot)]
        public void GetBestMMForMacdFlippingBot(Timeframe timeframe, int days, StrategyType strategyType,
            BotType botType)
        {
            var result = new List<Tuple<string, decimal, decimal, decimal, decimal>>();
            var errors = new List<string>();
            var options = new ParallelOptions()
            {
                MaxDegreeOfParallelism = 4
            };

            var stopLossRange = new List<decimal>() { 0.005m, 0.05m, 0.005m };
            var takeProfitRange = new List<decimal>() { 0.01m, 0.1m, 0.02m };
            var fileIdentifier = $"{strategyType}-{timeframe}-{botType}";
            var completedTest = 0;
            var totalTests = GetTotalTradeForStopLossTakeProfit(stopLossRange, takeProfitRange) *
                             Enum.GetNames(typeof(Ticker)).Length;

            CleanAnalyseFile(fileIdentifier);
            UpdateProgression(totalTests, completedTest);

            Parallel.ForEach((Ticker[])Enum.GetValues(typeof(Ticker)), options, ticker =>
            {
                var candles = _exchangeService
                    .GetCandles(_account, ticker, DateTime.Now.AddDays(Convert.ToDouble(days)), timeframe, true).Result;

                if (candles == null || candles.Count == 0)
                    return;

                var scenario = new Scenario("ScalpingScenario");
                var strategy = ScenarioHelpers.BuildStrategy(strategyType, "RsiDiv", fastPeriods: 12,
                    slowPeriods: 26, signalPeriods: 9);
                scenario.AddStrategy(strategy);

                // -0.5 to -5
                for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
                {
                    // +1% to +10% in 1%
                    for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2])
                    {
                        var timer = new Stopwatch();
                        timer.Start();
                        try
                        {
                            var moneyManagement = new MoneyManagement()
                            {
                                Leverage = 1,
                                Timeframe = timeframe,
                                StopLoss = s,
                                TakeProfit = t
                            };

                            var backtestResult = botType switch
                            {
                                BotType.SimpleBot => throw new NotImplementedException(),
                                BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(_account, moneyManagement,
                                    scenario, timeframe, candles, 1000, null).Result,
                                BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
                                    scenario, timeframe, candles, 1000, null).Result,
                                _ => throw new NotImplementedException(),
                            };

                            if (backtestResult.FinalPnl > 0
                                && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
                                && backtestResult.Statistics.MaxDrawdown < 3)
                            {
                                var currentResult = new Tuple<string, decimal, decimal, decimal, decimal>(
                                    ticker.ToString(),
                                    backtestResult.FinalPnl, s, t,
                                    backtestResult.GrowthPercentage - backtestResult.HodlPercentage);
                                result.Add(currentResult);
                            }

                            completedTest++;
                            UpdateProgression(totalTests, completedTest, timer.Elapsed.TotalSeconds);
                        }
                        catch (Exception ex)
                        {
                            completedTest++;
                            errors.Add($"{ticker}{_s}{s}{_s}{t}{_s}{ex.Message}");
                        }

                        timer.Stop();
                    }
                }
            });

            foreach (var r in result)
            {
                WriteCsvAnalyse(
                    $"{r.Item1}{_s}{r.Item2.ToString("0.000")}{_s}{r.Item3 * 100}{_s}{r.Item4 * 100}{_s}{r.Item5}");
            }

            foreach (var e in errors)
            {
                WriteCsvErrors(e);
            }

            var bestResult = result.OrderByDescending(b => b.Item3).FirstOrDefault();
            WriteCsvAnalyse(
                $"Best result : {bestResult.Item1} - Rsi Period : {bestResult.Item2} - {bestResult.Item3} - SL : {bestResult.Item4}% - TP : {bestResult.Item5}%");

            Assert.True(result.Any());
        }


        private void WriteCsvReport(string line)
        {
            File.AppendAllLines(_reportPath, new[] { line });
        }

        private void WriteCsvAnalyse(string line, string fileIdentifier = null)
        {
            if (!string.IsNullOrEmpty(fileIdentifier))
                _analysePath += $"-{fileIdentifier}-{DateTime.Now.ToString("dd-MM-HH-mm")}.csv";

            File.AppendAllLines(_analysePath, new[] { line });
        }

        private void WriteCsvErrors(string line)
        {
            File.AppendAllLines(_errorsPath, new[] { line });
        }

        private void CleanAnalyseFile(string fileIdentifier)
        {
            WriteCsvAnalyse("", fileIdentifier);
        }

        private decimal GetTotalTrades(List<int> periodRange, List<decimal> stopLossRange,
            List<decimal> takeProfitRange)
        {
            var stopLossRangeTotalTest = stopLossRange[1] / stopLossRange[2];
            var takeProfitRangeTotalTest = takeProfitRange[1] / takeProfitRange[2];

            var totalTrades = GetTotalTradeForStopLossTakeProfit(stopLossRange, takeProfitRange) *
                              stopLossRangeTotalTest * takeProfitRangeTotalTest;
            return totalTrades;
        }

        private decimal GetTotalTradeForStopLossTakeProfit(List<decimal> stopLossRange, List<decimal> takeProfitRange)
        {
            var stopLossRangeTotalTest = stopLossRange[1] / stopLossRange[2];
            var takeProfitRangeTotalTest = takeProfitRange[1] / takeProfitRange[2];

            var totalTrades = stopLossRangeTotalTest * takeProfitRangeTotalTest;
            return totalTrades;
        }


        private void UpdateProgression(decimal totalTest, int completedTest, double? elapsed = null)
        {
            var timeRemaining = "";
            if (elapsed.HasValue && completedTest > 0)
            {
                //_elapsedTimes.Add((elapsed.Value / completedTest) * ((double)totalTest - completedTest));
                _elapsedTimes.Add(elapsed.Value);
                //var t = (_elapsedTimes.Average() / completedTest) * ((double)totalTest - completedTest);
                var t = (_elapsedTimes.Average() * (double)(totalTest - completedTest));
                timeRemaining = $"   Remaining time: {t} seconds - Estimated end date: {DateTime.Now.AddSeconds(t)}";
            }

            ModifyFirstRow(_analysePath, $"{completedTest}/{totalTest}{timeRemaining}");
        }

        private void ModifyFirstRow(string filepath, string newValue)
        {
            ArrayList rows = new ArrayList();

            using (StreamReader reader = new StreamReader(filepath))
            {
                string row = null;

                while ((row = reader.ReadLine()) != null)
                {
                    rows.Add(row);
                }
            }

            // Determ if the file even contains rows.
            if (rows.Count > 0)
            {
                // Replace the first row.
                rows[0] = newValue;
            }
            else
            {
                // Add the new value because there
                // where no rows found in the file.
                rows.Add(newValue);
            }

            // Write the modified content to the file.
            using (StreamWriter writer = new StreamWriter(filepath, false))
            {
                foreach (String row in rows)
                {
                    writer.WriteLine(row);
                }
            }
        }
    }
}