package exchangewrapperstandards

import (
	"context"
	"errors"
	"os"
	"reflect"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/key"
	"github.com/thrasher-corp/gocryptotrader/config"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/dispatch"
	"github.com/thrasher-corp/gocryptotrader/engine"
	"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
	"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
	"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
	"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
	"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
	"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
	"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
	"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)

func TestMain(m *testing.M) {
	// only run testing suite for one CI/CD environment
	if skipAdditionalWrapperCITests() {
		return
	}
	os.Exit(m.Run())
}

// singleExchangeOverride enter an exchange name to only test that exchange
var singleExchangeOverride = ""

func TestAllExchangeWrappers(t *testing.T) {
	t.Parallel()
	cfg := config.GetConfig()
	err := cfg.LoadConfig("../../testdata/configtest.json", true)
	require.NoError(t, err, "LoadConfig must not error")
	err = dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit)
	require.NoError(t, err, "dispatch.EnsureRunning must not error")
	for i := range cfg.Exchanges {
		name := strings.ToLower(cfg.Exchanges[i].Name)
		t.Run(name+" wrapper tests", func(t *testing.T) {
			t.Parallel()
			if slices.Contains(unsupportedExchangeNames, name) {
				t.Skipf("skipping unsupported exchange %v", name)
			}
			if singleExchangeOverride != "" && name != singleExchangeOverride {
				t.Skip("skipping ", name, " due to override")
			}
			ctx := t.Context()
			if isCITest() && slices.Contains(blockedCIExchanges, name) {
				// rather than skipping tests where execution is blocked, provide an expired
				// context, so no executions can take place
				var cancelFn context.CancelFunc
				ctx, cancelFn = context.WithTimeout(ctx, 0)
				cancelFn()
			}
			exch, assetPairs := setupExchange(ctx, t, name, cfg)
			executeExchangeWrapperTests(ctx, t, exch, assetPairs)
		})
	}
}

func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.Config) (exchange.IBotExchange, []assetPair) {
	t.Helper()
	em := engine.NewExchangeManager()
	exch, err := em.NewExchangeByName(name)
	if err != nil {
		t.Fatalf("Cannot setup %v NewExchangeByName  %v", name, err)
	}
	var exchCfg *config.Exchange
	exchCfg, err = cfg.GetExchangeConfig(name)
	if err != nil {
		t.Fatalf("Cannot setup %v GetExchangeConfig %v", name, err)
	}
	exch.SetDefaults()
	exchCfg.API.AuthenticatedSupport = true
	exchCfg.API.Credentials = getExchangeCredentials(name)
	err = exch.Setup(exchCfg)
	if err != nil {
		t.Fatalf("Cannot setup %v exchange Setup %v", name, err)
	}

	err = exch.UpdateTradablePairs(ctx)
	require.Truef(t, errors.Is(err, context.DeadlineExceeded) || err == nil, "Exchange %s UpdateTradablePairs must not error: %s", name, err)
	b := exch.GetBase()
	assets := b.CurrencyPairs.GetAssetTypes(false)
	require.NotEmptyf(t, assets, "Exchange %s must have assets", name)
	for _, a := range assets {
		require.NoErrorf(t, b.CurrencyPairs.SetAssetEnabled(a, true), "Exchange %s SetAssetEnabled must not error for asset %s: %s", name, a, err)
	}
	// Add +1 to len to verify that exchanges can handle requests with unset pairs and assets
	assetPairs := make([]assetPair, 0, len(assets)+1)
assets:
	for j := range assets {
		var pairs currency.Pairs
		pairs, err = b.CurrencyPairs.GetPairs(assets[j], false)
		if err != nil {
			t.Fatalf("Cannot setup %v asset %v GetPairs %v", name, assets[j], err)
		}
		var p currency.Pair
		p, err = getPairFromPairs(t, pairs)
		if err != nil {
			if errors.Is(err, currency.ErrCurrencyPairsEmpty) {
				continue
			}
			t.Fatalf("Cannot setup %v asset %v getPairFromPairs %v", name, assets[j], err)
		}
		err = b.CurrencyPairs.EnablePair(assets[j], p)
		require.Truef(t, errors.Is(err, currency.ErrPairAlreadyEnabled) || err == nil, "Exchange %s EnablePair must not error for %s", name, p)
		p, err = b.FormatExchangeCurrency(p, assets[j])
		if err != nil {
			t.Fatalf("Cannot setup %v asset %v FormatExchangeCurrency %v", name, assets[j], err)
		}
		for x := range unsupportedAssets {
			if assets[j] == unsupportedAssets[x] {
				// this asset cannot handle disrupt formatting
				continue assets
			}
		}
		p, err = disruptFormatting(t, p)
		if err != nil {
			t.Fatalf("Cannot setup %v asset %v disruptFormatting %v", name, assets[j], err)
		}
		assetPairs = append(assetPairs, assetPair{
			Pair:  p,
			Asset: assets[j],
		})
	}
	assetPairs = append(assetPairs, assetPair{})

	return exch, assetPairs
}

// isUnacceptableError sentences errs to 10 years dungeon if unacceptable
func isUnacceptableError(t *testing.T, err error) error {
	t.Helper()
	for i := range acceptableErrors {
		if errors.Is(err, acceptableErrors[i]) {
			return nil
		}
	}
	for i := range warningErrors {
		if errors.Is(err, warningErrors[i]) {
			t.Log(err)
			return nil
		}
	}
	return err
}

var validWrapperParams = []reflect.Type{
	assetParam,
	orderSubmitParam,
	orderModifyParam,
	orderCancelParam,
	orderCancelsParam,
	pairKeySliceParam,
	getOrdersRequestParam,
	latestRateRequest,
}

type testCtxKey string

func executeExchangeWrapperTests(ctx context.Context, t *testing.T, exch exchange.IBotExchange, assetParams []assetPair) {
	t.Helper()
	iExchange := reflect.TypeOf(&exch).Elem()
	actualExchange := reflect.ValueOf(exch)
	for x := range iExchange.NumMethod() {
		methodName := iExchange.Method(x).Name
		if _, ok := excludedMethodNames[methodName]; ok {
			continue
		}
		method := actualExchange.MethodByName(methodName)
		var assetLen int
		for y := range method.Type().NumIn() {
			input := method.Type().In(y)
			if slices.ContainsFunc(validWrapperParams, func(t reflect.Type) bool {
				return input.AssignableTo(t)
			}) {
				assetLen = len(assetParams)
				break
			}
		}
		tt := time.Now()
		e := time.Date(tt.Year(), tt.Month(), tt.Day()-1, 0, 0, 0, 0, time.UTC)
		s := e.Add(-time.Hour * 24 * 2)
		if methodName == "GetHistoricTrades" {
			// limit trade history
			e = time.Now()
			s = e.Add(-time.Minute * 3)
		}
		for y := range assetLen {
			ap := assetParams[y]
			t.Run(methodName+"-"+ap.Asset.String()+"-"+ap.Pair.String(), func(t *testing.T) {
				t.Parallel()
				// Create a new context for each test run to avoid race conditions
				ctx := context.WithValue(ctx, testCtxKey("test"), t.Name()) //nolint:govet // Intentional shadow
				inputs := make([]reflect.Value, method.Type().NumIn())
				argGenerator := &MethodArgumentGenerator{
					Exchange:    exch,
					AssetParams: ap,
					MethodName:  methodName,
					Start:       s,
					End:         e,
				}
				for z := range method.Type().NumIn() {
					argGenerator.MethodInputType = method.Type().In(z)
					generatedArg := generateMethodArg(ctx, t, argGenerator)
					inputs[z] = *generatedArg
				}
				CallExchangeMethod(t, method, inputs, methodName, exch)
			})
		}
	}
}

// CallExchangeMethod will call an exchange's method using generated arguments
// and determine if the error is friendly
func CallExchangeMethod(t *testing.T, methodToCall reflect.Value, methodValues []reflect.Value, methodName string, exch exchange.IBotExchange) {
	t.Helper()
	outputs := methodToCall.Call(methodValues)
	for i := range outputs {
		outputInterface := outputs[i].Interface()
		err, ok := outputInterface.(error)
		if !ok {
			continue
		}
		if isUnacceptableError(t, err) != nil {
			literalInputs := make([]any, len(methodValues))
			for j := range methodValues {
				switch {
				case methodValues[j].Type().Implements(contextParam):
					// Errorf will use reflection on ctx and cause a race, so we need to replace it
					literalInputs[j] = "<context>"
				case methodValues[j].Kind() == reflect.Ptr:
					// dereference pointers just to add a bit more clarity
					literalInputs[j] = methodValues[j].Elem().Interface()
				default:
					literalInputs[j] = methodValues[j].Interface()
				}
			}
			t.Errorf("%v Func '%v' Error: '%v'. Inputs: %v.", exch.GetName(), methodName, err, literalInputs)
		}
		break
	}
}

// MethodArgumentGenerator is used to create arguments for
// an IBotExchange method
type MethodArgumentGenerator struct {
	Exchange        exchange.IBotExchange
	AssetParams     assetPair
	MethodInputType reflect.Type
	MethodName      string
	Start           time.Time
	End             time.Time
	StartTimeSet    bool
	argNum          int64
}

var (
	currencyPairParam    = reflect.TypeOf((*currency.Pair)(nil)).Elem()
	klineParam           = reflect.TypeOf((*kline.Interval)(nil)).Elem()
	contextParam         = reflect.TypeOf((*context.Context)(nil)).Elem()
	timeParam            = reflect.TypeOf((*time.Time)(nil)).Elem()
	codeParam            = reflect.TypeOf((*currency.Code)(nil)).Elem()
	currencyPairsParam   = reflect.TypeOf((*currency.Pairs)(nil)).Elem()
	withdrawRequestParam = reflect.TypeOf((**withdraw.Request)(nil)).Elem()
	stringParam          = reflect.TypeOf((*string)(nil)).Elem()
	feeBuilderParam      = reflect.TypeOf((**exchange.FeeBuilder)(nil)).Elem()
	credentialsParam     = reflect.TypeOf((**accounts.Credentials)(nil)).Elem()
	orderSideParam       = reflect.TypeOf((*order.Side)(nil)).Elem()
	collateralModeParam  = reflect.TypeOf((*collateral.Mode)(nil)).Elem()
	marginTypeParam      = reflect.TypeOf((*margin.Type)(nil)).Elem()
	int64Param           = reflect.TypeOf((*int64)(nil)).Elem()
	float64Param         = reflect.TypeOf((*float64)(nil)).Elem()
	// types with asset in params
	assetParam                  = reflect.TypeOf((*asset.Item)(nil)).Elem()
	orderSubmitParam            = reflect.TypeOf((**order.Submit)(nil)).Elem()
	orderModifyParam            = reflect.TypeOf((**order.Modify)(nil)).Elem()
	orderCancelParam            = reflect.TypeOf((**order.Cancel)(nil)).Elem()
	orderCancelsParam           = reflect.TypeOf((*[]order.Cancel)(nil)).Elem()
	getOrdersRequestParam       = reflect.TypeOf((**order.MultiOrderRequest)(nil)).Elem()
	positionChangeRequestParam  = reflect.TypeOf((**margin.PositionChangeRequest)(nil)).Elem()
	positionSummaryRequestParam = reflect.TypeOf((**futures.PositionSummaryRequest)(nil)).Elem()
	positionsRequestParam       = reflect.TypeOf((**futures.PositionsRequest)(nil)).Elem()
	latestRateRequest           = reflect.TypeOf((**fundingrate.LatestRateRequest)(nil)).Elem()
	pairKeySliceParam           = reflect.TypeOf((*[]key.PairAsset)(nil)).Elem()
)

// generateMethodArg determines the argument type and returns a pre-made
// response, else an empty version of the type
func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodArgumentGenerator) *reflect.Value {
	t.Helper()
	exchName := strings.ToLower(argGenerator.Exchange.GetName())
	var input reflect.Value
	switch {
	case argGenerator.MethodInputType.AssignableTo(stringParam):
		switch argGenerator.MethodName {
		case "GetDepositAddress":
			if argGenerator.argNum == 2 {
				// account type
				input = reflect.ValueOf("trading")
			} else {
				// Crypto Chain
				input = reflect.ValueOf(cryptoChainPerExchange[exchName])
			}
		case "MatchSymbolWithAvailablePairs", "MatchSymbolCheckEnabled":
			input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base.Lower().String() + argGenerator.AssetParams.Pair.Quote.Lower().String())
		default:
			// OrderID
			input = reflect.ValueOf("1337")
		}
	case argGenerator.MethodInputType.AssignableTo(pairKeySliceParam):
		input = reflect.ValueOf(key.PairAsset{
			Base:  argGenerator.AssetParams.Pair.Base.Item,
			Quote: argGenerator.AssetParams.Pair.Quote.Item,
			Asset: argGenerator.AssetParams.Asset,
		})
	case argGenerator.MethodInputType.AssignableTo(credentialsParam):
		input = reflect.ValueOf(&accounts.Credentials{
			Key:             "test",
			Secret:          "test",
			ClientID:        "test",
			PEMKey:          "test",
			SubAccount:      "test",
			OneTimePassword: "test",
		})
	case argGenerator.MethodInputType.Implements(contextParam):
		// Need to deploy a context.Context value as nil value is not checked throughout codebase
		input = reflect.ValueOf(ctx)
	case argGenerator.MethodInputType.AssignableTo(feeBuilderParam):
		input = reflect.ValueOf(&exchange.FeeBuilder{
			FeeType:       exchange.OfflineTradeFee,
			Amount:        150,
			PurchasePrice: 150,
			Pair:          argGenerator.AssetParams.Pair,
		})
	case argGenerator.MethodInputType.AssignableTo(currencyPairParam):
		input = reflect.ValueOf(argGenerator.AssetParams.Pair)
	case argGenerator.MethodInputType.AssignableTo(assetParam):
		input = reflect.ValueOf(argGenerator.AssetParams.Asset)
	case argGenerator.MethodInputType.AssignableTo(klineParam):
		input = reflect.ValueOf(kline.OneDay)
	case argGenerator.MethodInputType.AssignableTo(codeParam):
		if argGenerator.MethodName == "GetAvailableTransferChains" {
			input = reflect.ValueOf(currency.ETH)
		} else {
			input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base)
		}
	case argGenerator.MethodInputType.AssignableTo(timeParam):
		if !argGenerator.StartTimeSet {
			input = reflect.ValueOf(argGenerator.Start)
			argGenerator.StartTimeSet = true
		} else {
			input = reflect.ValueOf(argGenerator.End)
		}
	case argGenerator.MethodInputType.AssignableTo(currencyPairsParam):
		b := argGenerator.Exchange.GetBase()
		if argGenerator.AssetParams.Asset != asset.Empty {
			input = reflect.ValueOf(b.CurrencyPairs.Pairs[argGenerator.AssetParams.Asset].Available)
		} else {
			input = reflect.ValueOf(currency.Pairs{
				argGenerator.AssetParams.Pair,
			})
		}
	case argGenerator.MethodInputType.AssignableTo(withdrawRequestParam):
		req := &withdraw.Request{
			Exchange:      exchName,
			Description:   "1337",
			Amount:        1,
			ClientOrderID: "1337",
			WalletID:      "7331",
		}
		if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" {
			req.Type = withdraw.Crypto
			switch {
			case !isFiat(t, argGenerator.AssetParams.Pair.Base.Item.Lower):
				req.Currency = argGenerator.AssetParams.Pair.Base
			case !isFiat(t, argGenerator.AssetParams.Pair.Quote.Item.Lower):
				req.Currency = argGenerator.AssetParams.Pair.Quote
			default:
				req.Currency = currency.ETH
			}

			req.Crypto = withdraw.CryptoRequest{
				Address:    "1337",
				AddressTag: "1337",
				Chain:      cryptoChainPerExchange[exchName],
			}
		} else {
			req.Type = withdraw.Fiat
			b := argGenerator.Exchange.GetBase()
			if len(b.Config.BaseCurrencies) > 0 {
				req.Currency = b.Config.BaseCurrencies[0]
			} else {
				req.Currency = currency.USD
			}
			req.Fiat = withdraw.FiatRequest{
				Bank: banking.Account{
					Enabled:             true,
					ID:                  "1337",
					BankName:            "1337",
					BankAddress:         "1337",
					BankPostalCode:      "1337",
					BankPostalCity:      "1337",
					BankCountry:         "1337",
					AccountName:         "1337",
					AccountNumber:       "1337",
					SWIFTCode:           "1337",
					IBAN:                "1337",
					BSBNumber:           "1337",
					BankCode:            1337,
					SupportedCurrencies: req.Currency.String(),
					SupportedExchanges:  exchName,
				},
				IsExpressWire:                 false,
				RequiresIntermediaryBank:      false,
				IntermediaryBankAccountNumber: 1338,
				IntermediaryBankName:          "1338",
				IntermediaryBankAddress:       "1338",
				IntermediaryBankCity:          "1338",
				IntermediaryBankCountry:       "1338",
				IntermediaryBankPostalCode:    "1338",
				IntermediarySwiftCode:         "1338",
				IntermediaryBankCode:          1338,
				IntermediaryIBAN:              "1338",
				WireCurrency:                  "1338",
			}
		}
		input = reflect.ValueOf(req)
	case argGenerator.MethodInputType.AssignableTo(orderSubmitParam):
		input = reflect.ValueOf(&order.Submit{
			Exchange:      exchName,
			Type:          order.Limit,
			Side:          order.Buy,
			Pair:          argGenerator.AssetParams.Pair,
			AssetType:     argGenerator.AssetParams.Asset,
			Price:         150,
			Amount:        1,
			ClientID:      "1337",
			ClientOrderID: "13371337",
			TimeInForce:   order.ImmediateOrCancel,
			Leverage:      1,
		})
	case argGenerator.MethodInputType.AssignableTo(orderModifyParam):
		input = reflect.ValueOf(&order.Modify{
			Exchange:      exchName,
			Type:          order.Limit,
			Side:          order.Buy,
			Pair:          argGenerator.AssetParams.Pair,
			AssetType:     argGenerator.AssetParams.Asset,
			Price:         150,
			Amount:        1,
			ClientOrderID: "13371337",
			OrderID:       "1337",
			TimeInForce:   order.ImmediateOrCancel,
		})
	case argGenerator.MethodInputType.AssignableTo(orderCancelParam):
		input = reflect.ValueOf(&order.Cancel{
			Exchange:  exchName,
			Type:      order.Limit,
			Side:      order.Buy,
			Pair:      argGenerator.AssetParams.Pair,
			AssetType: argGenerator.AssetParams.Asset,
			OrderID:   "1337",
		})
	case argGenerator.MethodInputType.AssignableTo(orderCancelsParam):
		input = reflect.ValueOf([]order.Cancel{
			{
				Exchange:  exchName,
				Type:      order.Market,
				Side:      order.Buy,
				Pair:      argGenerator.AssetParams.Pair,
				AssetType: argGenerator.AssetParams.Asset,
				OrderID:   "1337",
			},
		})
	case argGenerator.MethodInputType.AssignableTo(getOrdersRequestParam):
		input = reflect.ValueOf(&order.MultiOrderRequest{
			Type:        order.AnyType,
			Side:        order.AnySide,
			FromOrderID: "1337",
			AssetType:   argGenerator.AssetParams.Asset,
			Pairs:       currency.Pairs{argGenerator.AssetParams.Pair},
		})
	case argGenerator.MethodInputType.AssignableTo(marginTypeParam):
		input = reflect.ValueOf(margin.Isolated)
	case argGenerator.MethodInputType.AssignableTo(collateralModeParam):
		input = reflect.ValueOf(collateral.SingleMode)
	case argGenerator.MethodInputType.AssignableTo(positionChangeRequestParam):
		input = reflect.ValueOf(&margin.PositionChangeRequest{
			Exchange:                argGenerator.Exchange.GetName(),
			Pair:                    argGenerator.AssetParams.Pair,
			Asset:                   argGenerator.AssetParams.Asset,
			MarginType:              margin.Isolated,
			OriginalAllocatedMargin: 150,
			NewAllocatedMargin:      151,
		})
	case argGenerator.MethodInputType.AssignableTo(positionSummaryRequestParam):
		input = reflect.ValueOf(&futures.PositionSummaryRequest{
			Asset:     argGenerator.AssetParams.Asset,
			Pair:      argGenerator.AssetParams.Pair,
			Direction: order.Buy,
		})
	case argGenerator.MethodInputType.AssignableTo(positionsRequestParam):
		input = reflect.ValueOf(&futures.PositionsRequest{
			Asset:                     argGenerator.AssetParams.Asset,
			Pairs:                     currency.Pairs{argGenerator.AssetParams.Pair},
			StartDate:                 argGenerator.Start,
			EndDate:                   argGenerator.End,
			RespectOrderHistoryLimits: true,
		})
	case argGenerator.MethodInputType.AssignableTo(orderSideParam):
		input = reflect.ValueOf(order.Long)
	case argGenerator.MethodInputType.AssignableTo(int64Param):
		input = reflect.ValueOf(150)
	case argGenerator.MethodInputType.AssignableTo(float64Param):
		input = reflect.ValueOf(150.0)
	case argGenerator.MethodInputType.AssignableTo(latestRateRequest):
		input = reflect.ValueOf(&fundingrate.LatestRateRequest{
			Asset:                argGenerator.AssetParams.Asset,
			Pair:                 argGenerator.AssetParams.Pair,
			IncludePredictedRate: true,
		})
	default:
		input = reflect.Zero(argGenerator.MethodInputType)
	}
	argGenerator.argNum++

	return &input
}

// assetPair holds a currency pair associated with an asset
type assetPair struct {
	Pair  currency.Pair
	Asset asset.Item
}

// excludedMethodNames represent the functions that are not
// currently tested under this suite due to irrelevance
// or not worth checking yet
var excludedMethodNames = map[string]struct{}{
	"Setup":                          {}, // Is run via test setup
	"Start":                          {}, // Is run via test setup
	"SetDefaults":                    {}, // Is run via test setup
	"UpdateTradablePairs":            {}, // Is run via test setup
	"GetDefaultConfig":               {}, // Is run via test setup
	"FetchTradablePairs":             {}, // Is run via test setup
	"AuthenticateWebsocket":          {}, // Unnecessary websocket test
	"FlushWebsocketChannels":         {}, // Unnecessary websocket test
	"UnsubscribeToWebsocketChannels": {}, // Unnecessary websocket test
	"SubscribeToWebsocketChannels":   {}, // Unnecessary websocket test
	"UpdateCurrencyStates":           {}, // Not widely supported/implemented feature
	"CanTradePair":                   {}, // Not widely supported/implemented feature
	"CanTrade":                       {}, // Not widely supported/implemented feature
	"CanWithdraw":                    {}, // Not widely supported/implemented feature
	"CanDeposit":                     {}, // Not widely supported/implemented feature
	"GetCurrencyStateSnapshot":       {}, // Not widely supported/implemented feature
	"SetHTTPClientUserAgent":         {}, // standard base implementation
	"SetClientProxyAddress":          {}, // standard base implementation
	// Not widely supported/implemented futures endpoints
	"GetCollateralCurrencyForContract": {},
	"GetCurrencyForRealisedPNL":        {},
	"GetFuturesPositions":              {},
	"GetHistoricalFundingRates":        {},
	"IsPerpetualFutureCurrency":        {},
	"GetMarginRatesHistory":            {},
	"CalculatePNL":                     {},
	"CalculateTotalCollateral":         {},
	"ScaleCollateral":                  {},
	"GetPositionSummary":               {},
	"GetFuturesPositionSummary":        {},
	"GetFuturesPositionOrders":         {},
	"SetCollateralMode":                {},
	"GetCollateralMode":                {},
	"SetLeverage":                      {},
	"GetLeverage":                      {},
	"SetMarginType":                    {},
	"ChangePositionMargin":             {},
}

// blockedCIExchanges are exchanges that are not able to be tested on CI
var blockedCIExchanges = []string{
	"binance", // binance API is banned from executing within the US where github Actions is ran
	"bybit",   // bybit API is banned from executing within the US where github Actions is ran
}

// unsupportedAssets contains assets that cannot handle
// normal processing for testing. This is to be used very sparingly
var unsupportedAssets = []asset.Item{
	asset.Index,
}

var unsupportedExchangeNames = []string{
	"testexch",
	"bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here
	"btse",     // 	TODO rm once timeout issues resolved
	"poloniex", // 	outdated API // TODO rm once updated
}

// cryptoChainPerExchange holds the deposit address chain per exchange
var cryptoChainPerExchange = map[string]string{
	"binanceus": "ERC20",
	"bybit":     "ERC20",
	"gateio":    "ERC20",
}

// acceptable errors do not throw test errors, see below for why
var acceptableErrors = []error{
	common.ErrFunctionNotSupported,       // Shows API cannot perform function and developer has recognised this
	common.ErrNotYetImplemented,          // Shows API can perform function but developer has not implemented it yet
	asset.ErrNotSupported,                // Shows that valid and invalid asset types are handled
	request.ErrAuthRequestFailed,         // We must set authenticated requests properly in order to understand and better handle auth failures
	order.ErrUnsupportedOrderType,        // Should be returned if an ordertype like ANY is requested and the implementation knows to throw this specific error
	currency.ErrCurrencyPairEmpty,        // Demonstrates handling of EMPTYPAIR scenario and returns the correct error
	currency.ErrCurrencyNotSupported,     // Ensures a standard error is used for when a particular currency/pair is not supported by an exchange
	currency.ErrCurrencyNotFound,         // Semi-randomly selected currency pairs may not be found at an endpoint, so long as this is returned it is okay
	asset.ErrNotEnabled,                  // Allows distinction when checking for supported versus enabled
	request.ErrRateLimiterAlreadyEnabled, // If the rate limiter is already enabled, it is not an error
	context.DeadlineExceeded,             // If the context deadline is exceeded, it is not an error as only blockedCIExchanges use expired contexts by design
	order.ErrPairIsEmpty,                 // Is thrown when the empty pair and asset scenario for an order submission is sent in the Validate() function
	deposit.ErrAddressNotFound,           // Is thrown when an address is not found due to the exchange requiring valid API keys
	futures.ErrNotFuturesAsset,           // Is thrown when a futures function receives a non-futures asset
	currency.ErrSymbolStringEmpty,        // Is thrown when a symbol string is empty for blank MatchSymbol func checks
	futures.ErrNotPerpetualFuture,        // Is thrown when a futures function receives a non-perpetual future
	limits.ErrExchangeLimitNotLoaded,     // Is thrown when the limits aren't loaded for a particular exchange, asset, pair
	limits.ErrOrderLimitNotFound,         // Is thrown when the order limit isn't found for a particular exchange, asset, pair
	limits.ErrEmptyLevels,                // Is thrown if limits are not provided for the asset
	limits.ErrPriceBelowMin,
	limits.ErrPriceExceedsMax,
	limits.ErrPriceExceedsStep,
	limits.ErrAmountBelowMin,
	limits.ErrAmountExceedsMax,
	limits.ErrAmountExceedsStep,
	limits.ErrNotionalValue,
	limits.ErrMarketAmountBelowMin,
	limits.ErrMarketAmountExceedsMax,
	limits.ErrMarketAmountExceedsStep,
	accounts.ErrNoBalances,
	accounts.ErrNoSubAccounts,
	ticker.ErrTickerNotFound,
	orderbook.ErrOrderbookNotFound,
	websocket.ErrNotConnected,
}

// warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest
// that the implementation is in error
var warningErrors = []error{
	kline.ErrNoTimeSeriesDataToConvert, // No data returned for a candle isn't worth failing the test suite over necessarily
}

// getPairFromPairs prioritises more normal pairs for an increased
// likelihood of returning data from API endpoints
func getPairFromPairs(t *testing.T, p currency.Pairs) (currency.Pair, error) {
	t.Helper()
	pFmt, err := p.GetFormatting()
	if err != nil {
		return currency.Pair{}, err
	}
	goodEth := currency.NewPair(currency.ETH, currency.USDT).Format(pFmt)
	if p.Contains(goodEth, true) {
		return goodEth, nil
	}
	for i := range p {
		if p[i].Base.Equal(currency.ETH) {
			return p[i], nil
		}
	}
	goodBtc := currency.NewBTCUSDT().Format(pFmt)
	if p.Contains(goodBtc, true) {
		return goodBtc, nil
	}
	for i := range p {
		if p[i].Base.Equal(currency.BTC) {
			return p[i], nil
		}
	}
	return p.GetRandomPair()
}

// isFiat helps determine fiat currency without using currency.storage
func isFiat(t *testing.T, c string) bool {
	t.Helper()
	fiats := []string{
		currency.USD.Item.Lower,
		currency.AUD.Item.Lower,
		currency.EUR.Item.Lower,
		currency.CAD.Item.Lower,
		currency.TRY.Item.Lower,
		currency.UAH.Item.Lower,
		currency.RUB.Item.Lower,
		currency.RUR.Item.Lower,
		currency.JPY.Item.Lower,
		currency.HKD.Item.Lower,
		currency.SGD.Item.Lower,
		currency.ZUSD.Item.Lower,
		currency.ZEUR.Item.Lower,
		currency.ZCAD.Item.Lower,
		currency.ZJPY.Item.Lower,
	}
	return slices.Contains(fiats, c)
}

// disruptFormatting adds in an unused delimiter and strange casing features to
// ensure format currency pair is used throughout the code base.
func disruptFormatting(t *testing.T, p currency.Pair) (currency.Pair, error) {
	t.Helper()
	if p.Base.IsEmpty() {
		return currency.EMPTYPAIR, errors.New("cannot disrupt formatting as base is not populated")
	}
	// NOTE: Quote can be empty for margin funding
	return currency.Pair{
		Base:      p.Base.Upper(),
		Quote:     p.Quote.Lower(),
		Delimiter: "-TEST-DELIM-",
	}, nil
}

func getExchangeCredentials(exchangeName string) config.APICredentialsConfig {
	var resp config.APICredentialsConfig
	switch exchangeName {
	case "lbank":
		// these are just random keys, they are not usable
		resp.Key = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3R2vuz3cpQUbCX0TgYZL
TiLSxUXdrvVEIyoqQyxNf+9fmHLEBrsO1s1msIKvWg24gdbLWXQ6NBCygO8OvZpm
+lfXD4MRv/0PxxIAkaD6Iplhv+qbae8nJkYQOpDJF3bPC9LCKfchCnRpZoGqkHgS
GqOBU13UDZ8BM1SaOLVBzcmE/iJCLPQPORNSzfLSb8TC+woe0AcaDmF9KjIzXPd0
Slacp1ZgZ+yIi1B5/akwxu6sGzHov6weXj/v9K8nUhL9+oPMd8FNzZ+z3viHY0fm
yWiHBywwlh4LgzrjGTUdUk9msjSr2rwjTdCp268A8ECC1fChvhdJfO3lYVj8ltDb
OQIDAQAB`
		resp.Secret = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdHa+7PdylBRsJ
fROBhktOItLFRd2u9UQjKipDLE1/71+YcsQGuw7WzWawgq9aDbiB1stZdDo0ELKA
7w69mmb6V9cPgxG//Q/HEgCRoPoimWG/6ptp7ycmRhA6kMkXds8L0sIp9yEKdGlm
gaqQeBIao4FTXdQNnwEzVJo4tUHNyYT+IkIs9A85E1LN8tJvxML7Ch7QBxoOYX0q
MjNc93RKVpynVmBn7IiLUHn9qTDG7qwbMei/rB5eP+/0rydSEv36g8x3wU3Nn7Pe
+IdjR+bJaIcHLDCWHguDOuMZNR1ST2ayNKvavCNN0KnbrwDwQILV8KG+F0l87eVh
WPyW0Ns5AgMBAAECggEAdBs7hJmWO7yzlsbrsC7BajUU8eue3VkCv2hLqtwfkdcz
HkzdLB+bSiWvD25//0yHHv6X5tAGJALEiLl+xwbFnhzz27xaXLLYTxLf45hg4Dwk
PO9HTlf6+bj+mpIeVcjYLYAs3nZbDi9UjTP3SUcTUpOavBjf2YstyTNai/55oEF/
x+ulzP/OISVhKrk5iiSKgjB4KyFpQnBWyluTmnlNS17/T/k6FkECQFgNpzbUmHTH
Yq+s0I9fGXMMvsNnnoJjX6ALe9fkMjY6ijeA45plDeBZp+5J8uGOKV+/iTCNzm5o
wrQKPz335+tTZgsDdKLUFA9Rwmkcpn4PShOtnR6aZQKBgQD9tzFlomqt/mSWbHAV
Gfjog9snlvgEWBIUjfP5Ow79rbz0cGcL3GAexwKK1dwNmMHDx+fu4uVAIf0dM5aT
xfdp/I4OTkxOFcIupu+L4gmz1vY32pFLPQYbp+9oOAMy4thUFb5o/Dsq2g65e2BC
+gNALEWxPuhNYbI7c0cu5Y7AJwKBgQDfG1ovhNlETJO+oli25csayRwgm/qll4fH
sOnYospQiJ3ka0WjPT6NY8m2anWDp7+/guIwq+xXVF6wQNxNZc+6/MgNJo2R3XG5
FKPH5FYgI52Zv6VN1AUhdfInDpKQXQ8vWO6HV+/uJmHeZK2+D6nycN4dL2h7ElK/
sCthmNtFnwKBgQDCGdaGpLzspAScOBV/b0FH0Shmn07bM+2RIBCYiaAsXzCB6URM
hKpcoW/Ge1pAZK9IcrVzws4URGx6XK9EGl3wDbE4LJqf2nGWc0wsPh+iIEB59pLV
drgnjFDR8Jgx4+4QVho4A0/Ytr4xFLxOQSsfez9OHIxoNue+J7E7pY+SXQKBgBTT
0tl4x2eO1oQHV8zLKui3OX750K5AtRY5N7tXhxd5iXPXZ8rTXtGILT5wNcQylr3k
FAWDJy8H20cM5wP6qyfDjVFc9f5V89XZTWjNshSR/pZpw56+WjRDdHWc8KW1akN7
Q9kypl1PC/fc4jNJ9w2A59tFn7VNgpgOdB5KTL31AoGBAN3BIjKXzoOJnVGL3bja
SYC2m+JcRn/mVO7I5Hop8GDoWXPFAnPNx1YKSpRLM/EV+ukUJsOV/LTPb7BsXMsJ
IY9SZceJS6glsxt+blFxGEpypyv13xW+jeCrPjlxQX2TNbL0KwHqvm1zMnM9bss/
Rsd80LrBCVI8ctzrvYRFSugC`
	default:
		resp.Key = "realKey"
		resp.Secret = "YXBpU2VjcmV0" // base64 encoded "apiSecret"
		resp.ClientID = "realClientID"
	}
	return resp
}

func isCITest() bool {
	return os.Getenv("CI") == "true"
}

func skipAdditionalWrapperCITests() bool {
	return os.Getenv("SKIP_WRAPPER_CI_TESTS") == "true"
}
