using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing.Comparers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace CryptoExchange.Net.Testing
{
    /// <summary>
    /// Base class for executing REST API integration tests
    /// </summary>
    /// <typeparam name="TClient">Client type</typeparam>
    public abstract class RestIntegrationTest<TClient>
    {
        /// <summary>
        /// Get a client instance
        /// </summary>
        public abstract TClient GetClient(ILoggerFactory loggerFactory);

        /// <summary>
        /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
        /// </summary>
        public virtual bool Run { get; set; }

        /// <summary>
        /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
        /// </summary>
        public bool Authenticated { get; set; }

        /// <summary>
        /// Create a client
        /// </summary>
        /// <returns></returns>
        protected TClient CreateClient()
        {
            var fact = new LoggerFactory();
            fact.AddProvider(new TraceLoggerProvider());
            return GetClient(fact);
        }

        /// <summary>
        /// Check if integration tests should be executed
        /// </summary>
        /// <returns></returns>
        protected bool ShouldRun()
        {
            var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
            if (!Run && integrationTests != "1")
                return false;

            return true;
        }

        /// <summary>
        /// Execute a REST endpoint call and check for any errors or warnings.
        /// </summary>
        /// <typeparam name="T">Type of response</typeparam>
        /// <param name="expression">The call expression</param>
        /// <param name="authRequest">Whether this is an authenticated request</param>
        /// <param name="checkMissingFields">Whether to check the response for missing fields</param>
        /// <param name="compareNestedProperty">Nested property to use for comparing when checking for missing fields</param>
        /// <param name="ignoreProperties">Properties to ignore when checking for missing fields</param>
        /// <param name="useSingleArrayItem">Whether to use the single array item as compare when checking for missing fields</param>
        public async Task RunAndCheckResult<T>(
            Expression<Func<TClient, Task<WebCallResult<T>>>> expression,
            bool authRequest,
            bool checkMissingFields = false,
            string? compareNestedProperty = null,
            List<string>? ignoreProperties = null,
            bool? useSingleArrayItem = null)
        {
            if (!ShouldRun())
                return;

            var client = CreateClient();

            var expressionBody = (MethodCallExpression)expression.Body;
            if (authRequest && !Authenticated)
            {
                Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
                return;
            }

            var listener = new EnumValueTraceListener();
            Trace.Listeners.Add(listener);

            WebCallResult<T> result;
            try
            {
                result = await expression.Compile().Invoke(client).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
            }
            finally
            {
                Trace.Listeners.Remove(listener);
            }

            if (!result.Success)
                throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);

            if (checkMissingFields)
            {
                var data = result.Data;
                var originalData = result.OriginalData;
                if (originalData == null)
                    throw new Exception($"Original data needs to be enabled in the client options to check for missing fields");

                try {
                    SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
                }
                catch (Exception ex)
                {
                    throw new Exception($"Compare failed: {ex.Message}; original data: {originalData}", ex);
                }
            }

            Debug.WriteLine($"{expressionBody.Method.Name} {result}");
        }

        /// <summary>
        /// Start an order book implementation and expect it to sync and produce an update
        /// </summary>
        public async Task TestOrderBook(ISymbolOrderBook book)
        {
            if (!ShouldRun())
                return;

            var bookHasChanged = false;
            book.OnStatusChange += (_, news) =>
            {
                if (news == OrderBookStatus.Reconnecting)
                    throw new Exception($"Book reconnecting");
            };
            book.OnOrderBookUpdate += (change) =>
            {
                bookHasChanged = true;
            };

            var result = await book.StartAsync().ConfigureAwait(false);
            if (!result)
                throw new Exception($"Book failed to start: " + result.Error);

            await Task.Delay(5000).ConfigureAwait(false);
            await book.StopAsync().ConfigureAwait(false);

            if (!bookHasChanged)
                throw new Exception($"Expected book to have changed at least once");
        }
    }
}
