package coinbase

import (
	"context"
	"errors"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/gofrs/uuid"
	gws "github.com/gorilla/websocket"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/config"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	"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/fundingrate"
	"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
	"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
	testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
	testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions"
	"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)

// Please supply your APIKeys here for better testing
const (
	apiKey                  = ""
	apiSecret               = ""
	canManipulateRealOrders = false
	// Sandbox functionality only works for certain endpoints https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/sandbox
	testingInSandbox = false
)

var (
	e                = &Exchange{}
	testCrypto       = currency.BTC
	testFiat         = currency.USD
	testStable       = currency.USDC
	testWrappedAsset = currency.CBETH
	testPairFiat     = currency.NewPairWithDelimiter(testCrypto.String(), testFiat.String(), "-")
	testPairStable   = currency.NewPairWithDelimiter(testCrypto.String(), testStable.String(), "-")
)

// Constants used within tests
const (
	testAddress = "fake address"
	testAmount  = 1e-08
	testAmount2 = 1e-02
	testAmount3 = 1
	testPrice   = 1.5e+05

	skipPayMethodNotFound          = "no payment methods found, skipping"
	skipInsufSuitableAccs          = "insufficient suitable accounts for test, skipping"
	skipInsufficientFunds          = "insufficient funds for test, skipping"
	skipInsufficientOrders         = "insufficient orders for test, skipping"
	skipInsufficientPortfolios     = "insufficient portfolios for test, skipping"
	skipInsufficientWallets        = "insufficient wallets for test, skipping"
	skipInsufficientFundsOrWallets = "insufficient funds or wallets for test, skipping"
	skipInsufficientTransactions   = "insufficient transactions for test, skipping"

	errExpectMismatch          = "received: '%v' but expected: '%v'"
	errExpectedNonEmpty        = "expected non-empty response"
	errInvalidProductID        = `Coinbase unsuccessful HTTP status code: 404 raw response: {"error":"NOT_FOUND","error_details":"valid product_id is required","message":"valid product_id is required"}`
	errExpectedFeeRange        = "expected fee range of %v and %v, received %v"
	errOptionInvalid           = `Coinbase unsuccessful HTTP status code: 400 raw response: {"error":"unknown","error_details":"parsing field \"product_type\": \"OPTIONS\" is not a valid value","message":"parsing field \"product_type\": \"OPTIONS\" is not a valid value"}`
	errJSONUnmarshalUnexpected = "JSON umarshalling did not return expected error"
)

func TestMain(m *testing.M) {
	if err := exchangeBaseHelper(e); err != nil {
		log.Fatal(err)
	}
	if testingInSandbox {
		err := e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
			exchange.RestSpot: sandboxAPIURL,
		})
		if err != nil {
			log.Fatalf("Coinbase SetDefaultEndpoints sandbox error: %s", err)
		}
	}
	os.Exit(m.Run())
}

func TestSetup(t *testing.T) {
	cfg, err := e.GetStandardConfig()
	assert.NoError(t, err)
	exch := &Exchange{}
	err = exchangeBaseHelper(exch)
	require.NoError(t, err)
	cfg.ProxyAddress = string(rune(0x7f))
	err = exch.Setup(cfg)
	assert.ErrorIs(t, err, exchange.ErrSettingProxyAddress)
}

func TestWsConnect(t *testing.T) {
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	exch := &Exchange{}
	exch.Websocket = sharedtestvalues.NewTestWebsocket()
	err := exch.WsConnect()
	assert.ErrorIs(t, err, websocket.ErrWebsocketNotEnabled)
	err = exchangeBaseHelper(exch)
	require.NoError(t, err)
	err = exch.Websocket.Enable()
	assert.NoError(t, err)
}

func TestGetAccountByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetAccountByID(t.Context(), "")
	assert.ErrorIs(t, err, errAccountIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	longResp, err := e.ListAccounts(t.Context(), 49, 0)
	require.NoError(t, err)
	require.True(t, longResp != nil && len(longResp.Accounts) > 0, errExpectedNonEmpty)
	shortResp, err := e.GetAccountByID(t.Context(), longResp.Accounts[0].UUID)
	require.NoError(t, err)
	assert.Equal(t, shortResp, longResp.Accounts[0])
}

func TestListAccounts(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.ListAccounts(t.Context(), 50, 0)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestCreateConvertQuote(t *testing.T) {
	t.Parallel()
	_, err := e.CreateConvertQuote(t.Context(), "", "", "", "", 0)
	assert.ErrorIs(t, err, errAccountIDEmpty)
	_, err = e.CreateConvertQuote(t.Context(), "meow", "123", "", "", 0)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	fromAccID, toAccID := convertTestHelper(t)
	resp, err := e.CreateConvertQuote(t.Context(), fromAccID, toAccID, "", "", 0.01)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestCommitConvertTrade(t *testing.T) {
	convertTestShared(t, e.CommitConvertTrade)
}

func TestGetConvertTradeByID(t *testing.T) {
	convertTestShared(t, e.GetConvertTradeByID)
}

func TestGetPermissions(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetPermissions(t.Context())
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetTransactionSummary(t *testing.T) {
	t.Parallel()
	_, err := e.GetTransactionSummary(t.Context(), time.Unix(2, 2), time.Unix(1, 1), "", "", "")
	assert.ErrorIs(t, err, common.ErrStartAfterEnd)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetTransactionSummary(t.Context(), time.Unix(1, 1), time.Now(), "UNKNOWN_VENUE_TYPE", asset.Spot.Upper(), "UNKNOWN_CONTRACT_EXPIRY_TYPE")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestCancelPendingFuturesSweep(t *testing.T) {
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	_, err := e.CancelPendingFuturesSweep(t.Context())
	assert.NoError(t, err)
}

func TestGetCurrentMarginWindow(t *testing.T) {
	t.Parallel()
	_, err := e.GetCurrentMarginWindow(t.Context(), "")
	assert.ErrorIs(t, err, errMarginProfileTypeEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetCurrentMarginWindow(t.Context(), "MARGIN_PROFILE_TYPE_RETAIL_REGULAR")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetFuturesBalanceSummary(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.GetFuturesBalanceSummary(t.Context())
	assert.NoError(t, err)
}

func TestGetFuturesPositionByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetFuturesPositionByID(t.Context(), currency.Pair{})
	assert.ErrorIs(t, err, errProductIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err = e.GetFuturesPositionByID(t.Context(), currency.NewPairWithDelimiter("SLR-25NOV25", "CDE", "-"))
	assert.NoError(t, err)
}

func TestGetIntradayMarginSetting(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.GetIntradayMarginSetting(t.Context())
	assert.NoError(t, err)
}

func TestListFuturesPositions(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.ListFuturesPositions(t.Context())
	assert.NoError(t, err)
}

func TestListFuturesSweeps(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.ListFuturesSweeps(t.Context())
	assert.NoError(t, err)
}

func TestScheduleFuturesSweep(t *testing.T) {
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	_, err := e.ScheduleFuturesSweep(t.Context(), 0.001337)
	assert.NoError(t, err)
}

func TestSetIntradayMarginSetting(t *testing.T) {
	t.Parallel()
	err := e.SetIntradayMarginSetting(t.Context(), "")
	assert.ErrorIs(t, err, errSettingEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	err = e.SetIntradayMarginSetting(t.Context(), "INTRADAY_MARGIN_SETTING_STANDARD")
	assert.NoError(t, err)
}

func TestCancelOrders(t *testing.T) {
	t.Parallel()
	var orderSlice []string
	_, err := e.CancelOrders(t.Context(), orderSlice)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	orderSlice = make([]string, 200)
	for i := range 200 {
		orderSlice[i] = strconv.Itoa(i)
	}
	_, err = e.CancelOrders(t.Context(), orderSlice)
	assert.ErrorIs(t, err, errCancelLimitExceeded)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	orderSlice = []string{"1"}
	resp, err := e.CancelOrders(t.Context(), orderSlice)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestClosePosition(t *testing.T) {
	t.Parallel()
	_, err := e.ClosePosition(t.Context(), "", currency.Pair{}, 0)
	assert.ErrorIs(t, err, order.ErrClientOrderIDMustBeSet)
	_, err = e.ClosePosition(t.Context(), "meow", currency.Pair{}, 0)
	assert.ErrorIs(t, err, errProductIDEmpty)
	_, err = e.ClosePosition(t.Context(), "meow", testPairFiat, 0)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	resp, err := e.ClosePosition(t.Context(), "1", currency.NewPairWithDelimiter("BIT", "31OCT25-CDE", "-"), testAmount)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestPlaceOrder(t *testing.T) {
	t.Parallel()
	ord := &PlaceOrderInfo{}
	_, err := e.PlaceOrder(t.Context(), ord)
	assert.ErrorIs(t, err, order.ErrClientOrderIDMustBeSet)
	ord.ClientOID = "meow"
	_, err = e.PlaceOrder(t.Context(), ord)
	assert.ErrorIs(t, err, errProductIDEmpty)
	ord.ProductID = testPairFiat.String()
	_, err = e.PlaceOrder(t.Context(), ord)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	ord.BaseAmount = testAmount
	_, err = e.PlaceOrder(t.Context(), ord)
	assert.ErrorIs(t, err, errInvalidOrderType)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	id, err := uuid.NewV4()
	assert.NoError(t, err)
	ord = &PlaceOrderInfo{
		ClientOID:  id.String(),
		ProductID:  testPairStable.String(),
		Side:       order.Buy.String(),
		MarginType: "CROSS",
		Leverage:   9999,
		OrderInfo: OrderInfo{
			PostOnly:   false,
			EndTime:    time.Now().Add(time.Hour),
			OrderType:  order.Limit,
			BaseAmount: testAmount,
			LimitPrice: testPrice,
		},
	}
	resp, err := e.PlaceOrder(t.Context(), ord)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	id, err = uuid.NewV4()
	assert.NoError(t, err)
	ord.ClientOID = id.String()
	ord.MarginType = "MULTI"
	resp, err = e.PlaceOrder(t.Context(), ord)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestEditOrder(t *testing.T) {
	t.Parallel()
	_, err := e.EditOrder(t.Context(), "", 0, 0)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	_, err = e.EditOrder(t.Context(), "meow", 0, 0)
	assert.ErrorIs(t, err, errSizeAndPriceZero)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	resp, err := e.EditOrder(t.Context(), "1", testAmount, testPrice-1)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestEditOrderPreview(t *testing.T) {
	t.Parallel()
	_, err := e.EditOrderPreview(t.Context(), "", 0, 0)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	_, err = e.EditOrderPreview(t.Context(), "meow", 0, 0)
	assert.ErrorIs(t, err, errSizeAndPriceZero)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.EditOrderPreview(t.Context(), "1", testAmount, testPrice+2)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetOrderByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetOrderByID(t.Context(), "", "", currency.Code{})
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	ordID, err := e.ListOrders(t.Context(), &ListOrdersReq{Limit: 10})
	assert.NoError(t, err)
	if ordID == nil || len(ordID.Orders) == 0 {
		t.Skip(skipInsufficientOrders)
	}
	resp, err := e.GetOrderByID(t.Context(), ordID.Orders[0].OrderID, ordID.Orders[0].ClientOID, testFiat)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestListFills(t *testing.T) {
	t.Parallel()
	_, err := e.ListFills(t.Context(), nil, nil, nil, 0, "", time.Unix(2, 2), time.Unix(1, 1), 0)
	assert.ErrorIs(t, err, common.ErrStartAfterEnd)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err = e.ListFills(t.Context(), nil, nil, currency.Pairs{testPairFiat, testPairStable}, 0, "TRADE_TIME", time.Time{}, time.Time{}, 0)
	assert.NoError(t, err)
	_, err = e.ListFills(t.Context(), []string{"1", "2"}, nil, nil, 0, "", time.Time{}, time.Time{}, 0)
	assert.NoError(t, err)
	_, err = e.ListFills(t.Context(), nil, []string{"3", "4"}, nil, 0, "", time.Time{}, time.Time{}, 0)
	assert.NoError(t, err)
}

func TestListOrders(t *testing.T) {
	t.Parallel()
	_, err := e.ListOrders(t.Context(), &ListOrdersReq{
		StartDate: time.Unix(2, 2),
		EndDate:   time.Unix(1, 1),
	})
	assert.ErrorIs(t, err, common.ErrStartAfterEnd)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	orderStatus := []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"}
	orderTypes := []string{"MARKET", "LIMIT", "STOP", "STOP_LIMIT", "BRACKET", "TWAP"}
	timeInForces := []string{"GOOD_UNTIL_DATE_TIME", "GOOD_UNTIL_CANCELLED", "IMMEDIATE_OR_CANCEL", "FILL_OR_KILL"}
	productIDs := currency.Pairs{testPairFiat, testPairStable}
	_, err = e.ListOrders(t.Context(), &ListOrdersReq{
		OrderStatus:          orderStatus,
		TimeInForces:         timeInForces,
		OrderTypes:           orderTypes,
		ProductIDs:           productIDs,
		OrderSide:            "BUY",
		OrderPlacementSource: "RETAIL_SIMPLE",
		ContractExpiryType:   "PERPETUAL",
		SortBy:               "LAST_FILL_TIME",
	})
	assert.NoError(t, err)
	resp, err := e.ListOrders(t.Context(), &ListOrdersReq{})
	assert.NoError(t, err)
	if resp == nil || len(resp.Orders) == 0 {
		t.Skip(skipInsufficientOrders)
	}
	orderIDs := []string{resp.Orders[0].OrderID}
	_, err = e.ListOrders(t.Context(), &ListOrdersReq{
		OrderIDs: orderIDs,
	})
	assert.NoError(t, err)
}

func TestPreviewOrder(t *testing.T) {
	t.Parallel()
	inf := &PreviewOrderInfo{}
	_, err := e.PreviewOrder(t.Context(), inf)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	inf.BaseAmount = 1
	_, err = e.PreviewOrder(t.Context(), inf)
	assert.ErrorIs(t, err, errInvalidOrderType)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	inf.ProductID = testPairStable.String()
	inf.Side = "BUY"
	inf.OrderType = order.Market
	inf.MarginType = "ISOLATED"
	inf.BaseAmount = testAmount
	resp, err := e.PreviewOrder(t.Context(), inf)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetPaymentMethodByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetPaymentMethodByID(t.Context(), "")
	assert.ErrorIs(t, err, errPaymentMethodEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	pmID, err := e.ListPaymentMethods(t.Context())
	assert.NoError(t, err)
	if len(pmID) == 0 {
		t.Skip(skipPayMethodNotFound)
	}
	resp, err := e.GetPaymentMethodByID(t.Context(), pmID[0].ID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestListPaymentMethods(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	testGetNoArgs(t, e.ListPaymentMethods)
}

func TestAllocatePortfolio(t *testing.T) {
	t.Parallel()
	err := e.AllocatePortfolio(t.Context(), "", "", "", 0)
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	err = e.AllocatePortfolio(t.Context(), "meow", "", "", 0)
	assert.ErrorIs(t, err, errProductIDEmpty)
	err = e.AllocatePortfolio(t.Context(), "meow", "bark", "", 0)
	assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
	err = e.AllocatePortfolio(t.Context(), "meow", "bark", "woof", 0)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	pID := getINTXPortfolio(t)
	err = e.AllocatePortfolio(t.Context(), pID, testCrypto.String(), testFiat.String(), 0.001337)
	assert.NoError(t, err)
}

func TestGetPerpetualsPortfolioSummary(t *testing.T) {
	t.Parallel()
	_, err := e.GetPerpetualsPortfolioSummary(t.Context(), "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	pID := getINTXPortfolio(t)
	resp, err := e.GetPerpetualsPortfolioSummary(t.Context(), pID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetPerpetualsPositionByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetPerpetualsPositionByID(t.Context(), "", currency.Pair{})
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	_, err = e.GetPerpetualsPositionByID(t.Context(), "meow", currency.Pair{})
	assert.ErrorIs(t, err, errProductIDEmpty)
	pID := getINTXPortfolio(t)
	_, err = e.GetPerpetualsPositionByID(t.Context(), pID, testPairFiat)
	assert.NoError(t, err)
}

func TestGetPortfolioBalances(t *testing.T) {
	t.Parallel()
	_, err := e.GetPortfolioBalances(t.Context(), "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	pID := getINTXPortfolio(t)
	resp, err := e.GetPortfolioBalances(t.Context(), pID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllPerpetualsPositions(t *testing.T) {
	t.Parallel()
	_, err := e.GetAllPerpetualsPositions(t.Context(), "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	pID := getINTXPortfolio(t)
	_, err = e.GetAllPerpetualsPositions(t.Context(), pID)
	assert.NoError(t, err)
}

func TestMultiAssetCollateralToggle(t *testing.T) {
	t.Parallel()
	_, err := e.MultiAssetCollateralToggle(t.Context(), "", false)
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	pID := getINTXPortfolio(t)
	_, err = e.MultiAssetCollateralToggle(t.Context(), pID, false)
	assert.NoError(t, err)
}

func TestCreatePortfolio(t *testing.T) {
	t.Parallel()
	_, err := e.CreatePortfolio(t.Context(), "")
	assert.ErrorIs(t, err, errNameEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	_, err = e.CreatePortfolio(t.Context(), "GCT Test Portfolio")
	assert.NoError(t, err)
}

func TestDeletePortfolio(t *testing.T) {
	t.Parallel()
	err := e.DeletePortfolio(t.Context(), "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	err = e.DeletePortfolio(t.Context(), "insert portfolio ID here")
	// The new JWT-based keys only have permissions to delete portfolios they're assigned to, causing this to fail
	assert.NoError(t, err)
}

func TestEditPortfolio(t *testing.T) {
	t.Parallel()
	_, err := e.EditPortfolio(t.Context(), "", "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	_, err = e.EditPortfolio(t.Context(), "meow", "")
	assert.ErrorIs(t, err, errNameEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	_, err = e.EditPortfolio(t.Context(), "insert portfolio ID here", "GCT Test Portfolio Edited")
	// The new JWT-based keys only have permissions to edit portfolios they're assigned to, causing this to fail
	assert.NoError(t, err)
}

func TestGetPortfolioByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetPortfolioByID(t.Context(), "")
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	portID, err := e.GetAllPortfolios(t.Context(), "")
	assert.NoError(t, err)
	if len(portID) == 0 {
		t.Fatal(errExpectedNonEmpty)
	}
	resp, err := e.GetPortfolioByID(t.Context(), portID[0].UUID)
	assert.NoError(t, err)
	if resp.Portfolio != portID[0] {
		t.Errorf(errExpectMismatch, resp.Portfolio, portID[0])
	}
}

func TestGetAllPortfolios(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetAllPortfolios(t.Context(), "DEFAULT")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestMovePortfolioFunds(t *testing.T) {
	t.Parallel()
	_, err := e.MovePortfolioFunds(t.Context(), currency.Code{}, "", "", 0)
	assert.ErrorIs(t, err, errPortfolioIDEmpty)
	_, err = e.MovePortfolioFunds(t.Context(), currency.Code{}, "meowPort", "woofPort", 0)
	assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
	_, err = e.MovePortfolioFunds(t.Context(), testCrypto, "meowPort", "woofPort", 0)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	portID, err := e.GetAllPortfolios(t.Context(), "")
	assert.NoError(t, err)
	if len(portID) < 2 {
		t.Skip(skipInsufficientPortfolios)
	}
	_, err = e.MovePortfolioFunds(t.Context(), testCrypto, portID[0].UUID, portID[1].UUID, testAmount)
	assert.NoError(t, err)
}

func TestGetBestBidAsk(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	testPairs := []string{testPairFiat.String(), "ETH-USD"}
	resp, err := e.GetBestBidAsk(t.Context(), testPairs)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetTicker(t *testing.T) {
	t.Parallel()
	_, err := e.GetTicker(t.Context(), currency.Pair{}, 1, time.Time{}, time.Time{}, false)
	assert.ErrorIs(t, err, errProductIDEmpty)
	resp, err := e.GetTicker(t.Context(), testPairFiat, 5, time.Now().Add(-time.Minute*5), time.Now(), false)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err = e.GetTicker(t.Context(), testPairFiat, 5, time.Now().Add(-time.Minute*5), time.Now(), true)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetProductByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetProductByID(t.Context(), currency.Pair{}, false)
	assert.ErrorIs(t, err, errProductIDEmpty)
	resp, err := e.GetProductByID(t.Context(), testPairFiat, false)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err = e.GetProductByID(t.Context(), testPairFiat, true)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetProductBookV3(t *testing.T) {
	t.Parallel()
	_, err := e.GetProductBookV3(t.Context(), currency.Pair{}, 0, 0, false)
	assert.ErrorIs(t, err, errProductIDEmpty)
	resp, err := e.GetProductBookV3(t.Context(), testPairFiat, 4, -1, false)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err = e.GetProductBookV3(t.Context(), testPairFiat, 4, -1, true)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetHistoricKlines(t *testing.T) {
	t.Parallel()
	_, err := e.GetHistoricKlines(t.Context(), "", kline.Raw, time.Time{}, time.Time{}, false)
	assert.ErrorIs(t, err, errProductIDEmpty)
	_, err = e.GetHistoricKlines(t.Context(), testPairFiat.String(), kline.Raw, time.Time{}, time.Time{}, false)
	assert.ErrorIs(t, err, kline.ErrUnsupportedInterval)
	resp, err := e.GetHistoricKlines(t.Context(), testPairFiat.String(), kline.OneMin, time.Now().Add(-5*time.Minute), time.Now(), false)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err = e.GetHistoricKlines(t.Context(), testPairFiat.String(), kline.OneMin, time.Now().Add(-5*time.Minute), time.Now(), true)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllProducts(t *testing.T) {
	t.Parallel()
	testPairs := []string{testPairFiat.String(), "ETH-USD"}
	resp, err := e.GetAllProducts(t.Context(), 30000, 1, "SPOT", "PERPETUAL", "STATUS_ALL", "PRODUCTS_SORT_ORDER_UNDEFINED", testPairs, true, true, false)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err = e.GetAllProducts(t.Context(), 0, 1, "SPOT", "PERPETUAL", "STATUS_ALL", "", nil, true, true, true)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetV3Time(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetV3Time)
}

func TestSendMoney(t *testing.T) {
	t.Parallel()
	_, err := e.SendMoney(t.Context(), "", "", "", "", "", "", "", currency.Code{}, 0, false, &TravelRule{})
	assert.ErrorIs(t, err, errTransactionTypeEmpty)
	_, err = e.SendMoney(t.Context(), "123", "", "", "", "", "", "", currency.Code{}, 0, false, &TravelRule{})
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.SendMoney(t.Context(), "123", "123", "", "", "", "", "", currency.Code{}, 0, false, &TravelRule{})
	assert.ErrorIs(t, err, errToEmpty)
	_, err = e.SendMoney(t.Context(), "123", "123", "123", "", "", "", "", currency.Code{}, 0, false, &TravelRule{})
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	_, err = e.SendMoney(t.Context(), "123", "123", "123", "", "", "", "", currency.Code{}, 1, false, &TravelRule{})
	assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wID, err := e.GetAllWallets(t.Context(), PaginationInp{})
	assert.NoError(t, err)
	if wID == nil || len(wID.Data) < 2 {
		t.Skip(skipInsufficientWallets)
	}
	var (
		fromID string
		toID   string
	)
	for i := range wID.Data {
		if wID.Data[i].Currency.Name == testCrypto.String() {
			if wID.Data[i].Balance.Amount > testAmount*100 {
				fromID = wID.Data[i].ID
			} else {
				toID = wID.Data[i].ID
			}
		}
		if fromID != "" && toID != "" {
			break
		}
	}
	if fromID == "" || toID == "" {
		t.Skip(skipInsufficientFundsOrWallets)
	}
	resp, err := e.SendMoney(t.Context(), "transfer", wID.Data[0].ID, wID.Data[1].ID, "GCT Test", "123", "", "", testCrypto, testAmount, false, &TravelRule{})
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestCreateAddress(t *testing.T) {
	t.Parallel()
	_, err := e.CreateAddress(t.Context(), "", "")
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	resp, err := e.CreateAddress(t.Context(), wID.ID, "")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllAddresses(t *testing.T) {
	t.Parallel()
	var pag PaginationInp
	_, err := e.GetAllAddresses(t.Context(), "", pag)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	resp, err := e.GetAllAddresses(t.Context(), wID.ID, pag)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAddressByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetAddressByID(t.Context(), "", "")
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.GetAddressByID(t.Context(), "123", "")
	assert.ErrorIs(t, err, errAddressIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	addID, err := e.GetAllAddresses(t.Context(), wID.ID, PaginationInp{})
	require.NoError(t, err)
	require.NotEmpty(t, addID, errExpectedNonEmpty)
	resp, err := e.GetAddressByID(t.Context(), wID.ID, addID.Data[0].ID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAddressTransactions(t *testing.T) {
	t.Parallel()
	_, err := e.GetAddressTransactions(t.Context(), "", "", PaginationInp{})
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.GetAddressTransactions(t.Context(), "123", "", PaginationInp{})
	assert.ErrorIs(t, err, errAddressIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	addID, err := e.GetAllAddresses(t.Context(), wID.ID, PaginationInp{})
	require.NoError(t, err)
	require.NotEmpty(t, addID, errExpectedNonEmpty)
	_, err = e.GetAddressTransactions(t.Context(), wID.ID, addID.Data[0].ID, PaginationInp{})
	assert.NoError(t, err)
}

func TestFiatTransfer(t *testing.T) {
	t.Parallel()
	_, err := e.FiatTransfer(t.Context(), "", "", "", 0, false, FiatDeposit)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.FiatTransfer(t.Context(), "123", "", "", 0, false, FiatDeposit)
	assert.ErrorIs(t, err, order.ErrAmountIsInvalid)
	_, err = e.FiatTransfer(t.Context(), "123", "", "", 1, false, FiatDeposit)
	assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
	_, err = e.FiatTransfer(t.Context(), "123", "123", "", 1, false, FiatDeposit)
	assert.ErrorIs(t, err, errPaymentMethodEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wallets, err := e.GetAllWallets(t.Context(), PaginationInp{})
	require.NoError(t, err)
	assert.NotEmpty(t, wallets, errExpectedNonEmpty)
	wID, pmID := transferTestHelper(t, wallets)
	resp, err := e.FiatTransfer(t.Context(), wID, testFiat.String(), pmID, testAmount, false, FiatDeposit)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	resp, err = e.FiatTransfer(t.Context(), wID, testFiat.String(), pmID, testAmount, false, FiatWithdrawal)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestCommitTransfer(t *testing.T) {
	t.Parallel()
	_, err := e.CommitTransfer(t.Context(), "", "", FiatDeposit)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.CommitTransfer(t.Context(), "123", "", FiatDeposit)
	assert.ErrorIs(t, err, errDepositIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wallets, err := e.GetAllWallets(t.Context(), PaginationInp{})
	require.NoError(t, err)
	assert.NotEmpty(t, wallets, errExpectedNonEmpty)
	wID, pmID := transferTestHelper(t, wallets)
	depID, err := e.FiatTransfer(t.Context(), wID, testFiat.String(), pmID, testAmount, false, FiatDeposit)
	require.NoError(t, err)
	resp, err := e.CommitTransfer(t.Context(), wID, depID.ID, FiatDeposit)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	depID, err = e.FiatTransfer(t.Context(), wID, testFiat.String(), pmID, testAmount, false, FiatWithdrawal)
	require.NoError(t, err)
	resp, err = e.CommitTransfer(t.Context(), wID, depID.ID, FiatWithdrawal)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllFiatTransfers(t *testing.T) {
	t.Parallel()
	var pag PaginationInp
	_, err := e.GetAllFiatTransfers(t.Context(), "", pag, FiatDeposit)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", currency.AUD)
	require.NoError(t, err)
	assert.NotEmpty(t, wID, errExpectedNonEmpty)
	// Fiat deposits/withdrawals aren't accepted for fiat currencies for Australian business accounts; the error "id not found" possibly reflects this
	_, err = e.GetAllFiatTransfers(t.Context(), wID.ID, pag, FiatDeposit)
	assert.NoError(t, err)
	_, err = e.GetAllFiatTransfers(t.Context(), wID.ID, pag, FiatWithdrawal)
	assert.NoError(t, err)
}

func TestGetFiatTransferByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetFiatTransferByID(t.Context(), "", "", FiatDeposit)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.GetFiatTransferByID(t.Context(), "123", "", FiatDeposit)
	assert.ErrorIs(t, err, errDepositIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", currency.AUD)
	require.NoError(t, err)
	assert.NotEmpty(t, wID, errExpectedNonEmpty)
	// Fiat deposits/withdrawals aren't accepted for fiat currencies for Australian business accounts; the error "id not found" possibly reflects this
	dID, err := e.GetAllFiatTransfers(t.Context(), wID.ID, PaginationInp{}, FiatDeposit)
	assert.NoError(t, err)
	if dID == nil || len(dID.Data) == 0 {
		t.Skip(skipInsufficientTransactions)
	}
	resp, err := e.GetFiatTransferByID(t.Context(), wID.ID, dID.Data[0].ID, FiatDeposit)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	resp, err = e.GetFiatTransferByID(t.Context(), wID.ID, dID.Data[0].ID, FiatWithdrawal)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllWallets(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	pagIn := PaginationInp{Limit: 2}
	resp, err := e.GetAllWallets(t.Context(), pagIn)
	assert.NoError(t, err)
	require.NotEmpty(t, resp, errExpectedNonEmpty)
	if resp.Pagination.NextStartingAfter == "" {
		t.Skip(skipInsufficientWallets)
	}
	pagIn.StartingAfter = resp.Pagination.NextStartingAfter
	resp, err = e.GetAllWallets(t.Context(), pagIn)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetWalletByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetWalletByID(t.Context(), "", currency.Code{})
	assert.ErrorIs(t, err, errCurrWalletConflict)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, resp, errExpectedNonEmpty)
	resp, err = e.GetWalletByID(t.Context(), resp.ID, currency.Code{})
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllTransactions(t *testing.T) {
	t.Parallel()
	var pag PaginationInp
	_, err := e.GetAllTransactions(t.Context(), "", pag)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	_, err = e.GetAllTransactions(t.Context(), wID.ID, pag)
	assert.NoError(t, err)
}

func TestGetTransactionByID(t *testing.T) {
	t.Parallel()
	_, err := e.GetTransactionByID(t.Context(), "", "")
	assert.ErrorIs(t, err, errWalletIDEmpty)
	_, err = e.GetTransactionByID(t.Context(), "123", "")
	assert.ErrorIs(t, err, errTransactionIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	wID, err := e.GetWalletByID(t.Context(), "", testCrypto)
	require.NoError(t, err)
	require.NotEmpty(t, wID, errExpectedNonEmpty)
	tID, err := e.GetAllTransactions(t.Context(), wID.ID, PaginationInp{})
	assert.NoError(t, err)
	if tID == nil || len(tID.Data) == 0 {
		t.Skip(skipInsufficientTransactions)
	}
	resp, err := e.GetTransactionByID(t.Context(), wID.ID, tID.Data[0].ID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetFiatCurrencies(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetFiatCurrencies)
}

func TestGetCryptocurrencies(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetCryptocurrencies)
}

func TestGetExchangeRates(t *testing.T) {
	t.Parallel()
	resp, err := e.GetExchangeRates(t.Context(), "")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetPrice(t *testing.T) {
	t.Parallel()
	_, err := e.GetPrice(t.Context(), "", "")
	assert.ErrorIs(t, err, errInvalidPriceType)
	resp, err := e.GetPrice(t.Context(), testPairFiat.String(), asset.Spot.String())
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetV2Time(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetV2Time)
}

func TestGetCurrentUser(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	// This intermittently fails with the message "Unauthorized", for no clear reason
	testGetNoArgs(t, e.GetCurrentUser)
}

func TestGetAllCurrencies(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetAllCurrencies)
}

func TestGetACurrency(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetACurrency, testCrypto.String(), currency.ErrCurrencyCodeEmpty)
}

func TestGetAllTradingPairs(t *testing.T) {
	t.Parallel()
	_, err := e.GetAllTradingPairs(t.Context(), "")
	assert.NoError(t, err)
}

func TestGetAllPairVolumes(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetAllPairVolumes)
}

func TestGetPairDetails(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetPairDetails, testPairFiat.String(), currency.ErrCurrencyPairEmpty)
}

func TestGetProductBookV1(t *testing.T) {
	t.Parallel()
	_, err := e.GetProductBookV1(t.Context(), "", 0)
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.GetProductBookV1(t.Context(), testPairFiat.String(), 2)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	resp, err = e.GetProductBookV1(t.Context(), testPairFiat.String(), 3)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetProductCandles(t *testing.T) {
	t.Parallel()
	_, err := e.GetProductCandles(t.Context(), "", 0, time.Time{}, time.Time{})
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.GetProductCandles(t.Context(), testPairFiat.String(), 300, time.Time{}, time.Time{})
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetProductStats(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetProductStats, testPairFiat.String(), currency.ErrCurrencyPairEmpty)
}

func TestGetProductTicker(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetProductTicker, testPairFiat.String(), currency.ErrCurrencyPairEmpty)
}

func TestGetProductTrades(t *testing.T) {
	t.Parallel()
	_, err := e.GetProductTrades(t.Context(), "", "", "", 0)
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.GetProductTrades(t.Context(), testPairFiat.String(), "1", "before", 0)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAllWrappedAssets(t *testing.T) {
	t.Parallel()
	testGetNoArgs(t, e.GetAllWrappedAssets)
}

func TestGetWrappedAssetDetails(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetWrappedAssetDetails, testWrappedAsset.String(), errWrappedAssetEmpty)
}

func TestGetWrappedAssetConversionRate(t *testing.T) {
	t.Parallel()
	testGetOneArg(t, e.GetWrappedAssetConversionRate, testWrappedAsset.String(), errWrappedAssetEmpty)
}

func TestSendHTTPRequest(t *testing.T) {
	t.Parallel()
	err := e.SendHTTPRequest(t.Context(), exchange.EdgeCase3, "", nil, nil)
	assert.ErrorIs(t, err, exchange.ErrEndpointPathNotFound)
}

func TestSendAuthenticatedHTTPRequest(t *testing.T) {
	t.Parallel()
	err := e.SendAuthenticatedHTTPRequest(t.Context(), exchange.EdgeCase3, "", "", nil, nil, false, nil)
	assert.ErrorIs(t, err, exchange.ErrEndpointPathNotFound)
	ch := make(chan struct{})
	body := map[string]any{"Unmarshalable": ch}
	err = e.SendAuthenticatedHTTPRequest(t.Context(), exchange.RestSpot, "", "", nil, body, false, nil)
	// TODO: Implement this more rigorously once thrasher investigates the code further
	// var targetErr *json.UnsupportedTypeError
	// assert.ErrorAs(t, err, &targetErr)
	assert.ErrorContains(t, err, "json: unsupported type: chan struct {}")
}

func TestGetFee(t *testing.T) {
	t.Parallel()
	_, err := e.GetFee(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	feeBuilder := exchange.FeeBuilder{
		FeeType:       exchange.OfflineTradeFee,
		Amount:        1,
		PurchasePrice: 1,
	}
	resp, err := e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if resp != WorstCaseTakerFee {
		t.Errorf(errExpectMismatch, resp, WorstCaseTakerFee)
	}
	feeBuilder.IsMaker = true
	resp, err = e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if resp != WorstCaseMakerFee {
		t.Errorf(errExpectMismatch, resp, WorstCaseMakerFee)
	}
	feeBuilder.Pair = currency.NewPair(currency.USDT, currency.USD)
	resp, err = e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if resp != 0 {
		t.Errorf(errExpectMismatch, resp, StablePairMakerFee)
	}
	feeBuilder.IsMaker = false
	resp, err = e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if resp != WorstCaseStablePairTakerFee {
		t.Errorf(errExpectMismatch, resp, WorstCaseStablePairTakerFee)
	}
	feeBuilder.FeeType = exchange.CryptocurrencyDepositFee
	_, err = e.GetFee(t.Context(), &feeBuilder)
	assert.ErrorIs(t, err, errFeeTypeNotSupported)
	feeBuilder.Pair = currency.Pair{}
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	feeBuilder.FeeType = exchange.CryptocurrencyTradeFee
	resp, err = e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if !(resp <= WorstCaseTakerFee && resp >= BestCaseTakerFee) {
		t.Errorf(errExpectedFeeRange, BestCaseTakerFee, WorstCaseTakerFee, resp)
	}
	feeBuilder.IsMaker = true
	resp, err = e.GetFee(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if !(resp <= WorstCaseMakerFee && resp >= BestCaseMakerFee) {
		t.Errorf(errExpectedFeeRange, BestCaseMakerFee, WorstCaseMakerFee, resp)
	}
}

func TestFetchTradablePairs(t *testing.T) {
	t.Parallel()
	_, err := e.FetchTradablePairs(t.Context(), asset.Options)
	assert.Equal(t, errOptionInvalid, err.Error())
	resp, err := e.FetchTradablePairs(t.Context(), asset.Spot)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	resp, err = e.FetchTradablePairs(t.Context(), asset.Futures)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestUpdateTradablePairs(t *testing.T) {
	t.Parallel()
	testexch.UpdatePairsOnce(t, e)
}

func TestUpdateAccountBalances(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.UpdateAccountBalances(t.Context(), asset.Spot)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestUpdateTicker(t *testing.T) {
	t.Parallel()
	_, err := e.UpdateTicker(t.Context(), currency.Pair{}, asset.Spot)
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.UpdateTicker(t.Context(), testPairFiat, asset.Spot)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

// Not parallel; being parallel causes intermittent errors with another test for no discernible reason
func TestUpdateOrderbook(t *testing.T) {
	testexch.UpdatePairsOnce(t, e)
	_, err := e.UpdateOrderbook(t.Context(), currency.Pair{}, asset.Empty)
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	_, err = e.UpdateOrderbook(t.Context(), testPairFiat, asset.Empty)
	assert.ErrorIs(t, err, asset.ErrNotSupported)
	_, err = e.UpdateOrderbook(t.Context(), currency.NewPairWithDelimiter("meow", "woof", "-"), asset.Spot)
	assert.Equal(t, errInvalidProductID, err.Error())
	// There are no perpetual futures contracts, so I can only deterministically test spot
	resp, err := e.UpdateOrderbook(t.Context(), testPairFiat, asset.Spot)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetAccountFundingHistory(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.GetAccountFundingHistory(t.Context())
	assert.NoError(t, err)
}

func TestGetWithdrawalsHistory(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.GetWithdrawalsHistory(t.Context(), currency.NewCode("meow"), asset.Spot)
	assert.ErrorIs(t, err, errNoMatchingWallets)
	_, err = e.GetWithdrawalsHistory(t.Context(), testCrypto, asset.Spot)
	assert.NoError(t, err)
}

func TestSubmitOrder(t *testing.T) {
	t.Parallel()
	_, err := e.SubmitOrder(t.Context(), nil)
	assert.ErrorIs(t, err, order.ErrSubmissionIsNil)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	ord := order.Submit{
		Exchange:      e.Name,
		Pair:          testPairStable,
		AssetType:     asset.Spot,
		Side:          order.Buy,
		Type:          order.StopLimit,
		StopDirection: order.StopUp,
		Amount:        testAmount2,
		Price:         testPrice,
		TriggerPrice:  testPrice + 1,
		RetrieveFees:  true,
		ClientOrderID: strconv.FormatInt(time.Now().UnixMilli(), 18) + "GCTSubmitOrderTest",
	}
	resp, err := e.SubmitOrder(t.Context(), &ord)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	ord.StopDirection = order.StopDown
	ord.TriggerPrice = testPrice/2 + 1
	resp, err = e.SubmitOrder(t.Context(), &ord)
	if assert.NoError(t, err) {
		assert.NotEmpty(t, resp, errExpectedNonEmpty)
	}
	ord.Type = order.Market
	ord.QuoteAmount = testAmount3
	resp, err = e.SubmitOrder(t.Context(), &ord)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestModifyOrder(t *testing.T) {
	t.Parallel()
	_, err := e.ModifyOrder(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	var ord order.Modify
	_, err = e.ModifyOrder(t.Context(), &ord)
	assert.ErrorIs(t, err, order.ErrPairIsEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	ord.OrderID = "a"
	ord.Price = testPrice + 1
	ord.Amount = testAmount
	ord.Pair = testPairStable
	ord.AssetType = asset.Spot
	resp2, err := e.ModifyOrder(t.Context(), &ord)
	require.NoError(t, err)
	assert.NotEmpty(t, resp2, errExpectedNonEmpty)
}

func TestCancelOrder(t *testing.T) {
	t.Parallel()
	err := e.CancelOrder(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	var can order.Cancel
	err = e.CancelOrder(t.Context(), &can)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	can.OrderID = "0"
	err = e.CancelOrder(t.Context(), &can)
	assert.Error(t, err)
	can.OrderID = "2"
	err = e.CancelOrder(t.Context(), &can)
	assert.NoError(t, err)
}

func TestCancelBatchOrders(t *testing.T) {
	t.Parallel()
	_, err := e.CancelBatchOrders(t.Context(), nil)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	can := make([]order.Cancel, 1)
	_, err = e.CancelBatchOrders(t.Context(), can)
	assert.ErrorIs(t, err, order.ErrOrderIDNotSet)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	can[0].OrderID = "1"
	resp2, err := e.CancelBatchOrders(t.Context(), can)
	require.NoError(t, err)
	assert.NotEmpty(t, resp2, errExpectedNonEmpty)
}

func TestGetOrderInfo(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	resp, err := e.GetOrderInfo(t.Context(), "17", testPairStable, asset.Spot)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetDepositAddress(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, err := e.GetDepositAddress(t.Context(), currency.NewCode("fake currency that doesn't exist"), "", "")
	assert.ErrorIs(t, err, errNoWalletForCurrency)
	resp, err := e.GetDepositAddress(t.Context(), testCrypto, "", "")
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestWithdrawCryptocurrencyFunds(t *testing.T) {
	t.Parallel()
	req := withdraw.Request{}
	_, err := e.WithdrawCryptocurrencyFunds(t.Context(), &req)
	assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
	req.Exchange = e.Name
	req.Currency = testCrypto
	req.Amount = testAmount
	req.Type = withdraw.Crypto
	req.Crypto.Address = testAddress
	_, err = e.WithdrawCryptocurrencyFunds(t.Context(), &req)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wallets, err := e.GetAllWallets(t.Context(), PaginationInp{})
	assert.NoError(t, err)
	if wallets == nil || len(wallets.Data) == 0 {
		t.Fatal(errExpectedNonEmpty)
	}
	for i := range wallets.Data {
		if wallets.Data[i].Currency.Name == testCrypto.String() && wallets.Data[i].Balance.Amount > testAmount*100 {
			req.WalletID = wallets.Data[i].ID
			break
		}
	}
	if req.WalletID == "" {
		t.Skip(skipInsufficientFunds)
	}
	resp, err := e.WithdrawCryptocurrencyFunds(t.Context(), &req)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestWithdrawFiatFunds(t *testing.T) {
	withdrawFiatFundsHelper(t, e.WithdrawFiatFunds)
}

func TestWithdrawFiatFundsToInternationalBank(t *testing.T) {
	withdrawFiatFundsHelper(t, e.WithdrawFiatFundsToInternationalBank)
}

func TestGetFeeByType(t *testing.T) {
	t.Parallel()
	_, err := e.GetFeeByType(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	var feeBuilder exchange.FeeBuilder
	feeBuilder.FeeType = exchange.OfflineTradeFee
	feeBuilder.Amount = 1
	feeBuilder.PurchasePrice = 1
	resp, err := e.GetFeeByType(t.Context(), &feeBuilder)
	assert.NoError(t, err)
	if resp != WorstCaseTakerFee {
		t.Errorf(errExpectMismatch, resp, WorstCaseTakerFee)
	}
}

func TestGetActiveOrders(t *testing.T) {
	t.Parallel()
	_, err := e.GetActiveOrders(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	var req order.MultiOrderRequest
	_, err = e.GetActiveOrders(t.Context(), &req)
	assert.ErrorIs(t, err, asset.ErrNotSupported)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	req.AssetType = asset.Spot
	req.Side = order.AnySide
	req.Type = order.AnyType
	_, err = e.GetActiveOrders(t.Context(), &req)
	assert.NoError(t, err)
	req.Pairs = req.Pairs.Add(currency.NewPair(testCrypto, testFiat))
	_, err = e.GetActiveOrders(t.Context(), &req)
	assert.NoError(t, err)
}

func TestGetOrderHistory(t *testing.T) {
	t.Parallel()
	_, err := e.GetOrderHistory(t.Context(), nil)
	assert.ErrorIs(t, err, order.ErrGetOrdersRequestIsNil)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	var req order.MultiOrderRequest
	req.AssetType = asset.Spot
	req.Side = order.AnySide
	req.Type = order.AnyType
	_, err = e.GetOrderHistory(t.Context(), &req)
	assert.NoError(t, err)
	req.Pairs = req.Pairs.Add(testPairStable)
	_, err = e.GetOrderHistory(t.Context(), &req)
	assert.NoError(t, err)
}

func TestGetHistoricCandles(t *testing.T) {
	t.Parallel()
	_, err := e.GetHistoricCandles(t.Context(), currency.Pair{}, asset.Empty, kline.OneYear, time.Time{}, time.Time{})
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.GetHistoricCandles(t.Context(), testPairFiat, asset.Spot, kline.SixHour, time.Now().Add(-time.Hour*60), time.Now())
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetHistoricCandlesExtended(t *testing.T) {
	t.Parallel()
	_, err := e.GetHistoricCandlesExtended(t.Context(), currency.Pair{}, asset.Empty, kline.OneYear, time.Time{}, time.Time{})
	assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
	resp, err := e.GetHistoricCandlesExtended(t.Context(), testPairFiat, asset.Spot, kline.OneMin, time.Now().Add(-time.Hour*9), time.Now())
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestValidateAPICredentials(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	err := e.ValidateAPICredentials(t.Context(), asset.Spot)
	assert.NoError(t, err)
}

func TestGetServerTime(t *testing.T) {
	t.Parallel()
	_, err := e.GetServerTime(t.Context(), 0)
	assert.NoError(t, err)
}

func TestGetLatestFundingRates(t *testing.T) {
	t.Parallel()
	_, err := e.GetLatestFundingRates(t.Context(), nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	req := fundingrate.LatestRateRequest{Asset: asset.UpsideProfitContract}
	_, err = e.GetLatestFundingRates(t.Context(), &req)
	assert.ErrorIs(t, err, asset.ErrNotSupported)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	req.Asset = asset.Futures
	resp, err := e.GetLatestFundingRates(t.Context(), &req)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestGetFuturesContractDetails(t *testing.T) {
	t.Parallel()
	_, err := e.GetFuturesContractDetails(t.Context(), asset.Empty)
	assert.ErrorIs(t, err, futures.ErrNotFuturesAsset)
	_, err = e.GetFuturesContractDetails(t.Context(), asset.UpsideProfitContract)
	assert.ErrorIs(t, err, asset.ErrNotSupported)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetFuturesContractDetails(t.Context(), asset.Futures)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

func TestUpdateOrderExecutionLimits(t *testing.T) {
	t.Parallel()
	err := e.UpdateOrderExecutionLimits(t.Context(), asset.Options)
	assert.ErrorIs(t, err, asset.ErrNotSupported)
	err = e.UpdateOrderExecutionLimits(t.Context(), asset.Spot)
	assert.NoError(t, err)
}

func TestGetOrderRespToOrderDetail(t *testing.T) {
	t.Parallel()
	mockData := &GetOrderResponse{
		OrderConfiguration: OrderConfiguration{
			MarketMarketIOC:       &MarketMarketIOC{},
			LimitLimitGTC:         &LimitLimitGTC{},
			LimitLimitGTD:         &LimitLimitGTD{},
			StopLimitStopLimitGTC: &StopLimitStopLimitGTC{},
			StopLimitStopLimitGTD: &StopLimitStopLimitGTD{},
		},
		SizeInQuote: false,
		Side:        "BUY",
		Status:      "OPEN",
		Settled:     true,
		EditHistory: []EditHistory{(EditHistory{})},
	}
	resp := e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected := &order.Detail{TimeInForce: order.ImmediateOrCancel, Exchange: "Coinbase", Type: order.StopLimit, Side: order.Buy, Status: order.Open, AssetType: asset.Spot, Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), CloseTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), LastUpdated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), Pair: testPairStable}
	assert.Equal(t, expected, resp)
	mockData.Side = "SELL"
	mockData.Status = "FILLED"
	resp = e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected.Side = order.Sell
	expected.Status = order.Filled
	assert.Equal(t, expected, resp)
	mockData.Status = "CANCELLED"
	resp = e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected.Status = order.Cancelled
	assert.Equal(t, expected, resp)
	mockData.Status = "EXPIRED"
	resp = e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected.Status = order.Expired
	assert.Equal(t, expected, resp)
	mockData.Status = "FAILED"
	resp = e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected.Status = order.Rejected
	assert.Equal(t, expected, resp)
	mockData.Status = "UNKNOWN_ORDER_STATUS"
	resp = e.getOrderRespToOrderDetail(mockData, testPairStable, asset.Spot)
	expected.Status = order.UnknownStatus
	assert.Equal(t, expected, resp)
}

func TestFiatTransferTypeString(t *testing.T) {
	t.Parallel()
	var f FiatTransferType
	if f.String() != "deposit" {
		t.Errorf(errExpectMismatch, f.String(), "deposit")
	}
	f = FiatWithdrawal
	if f.String() != "withdrawal" {
		t.Errorf(errExpectMismatch, f.String(), "withdrawal")
	}
}

func TestGetCurrencyTradeURL(t *testing.T) {
	t.Parallel()
	testexch.UpdatePairsOnce(t, e)
	for _, a := range e.GetAssetTypes(false) {
		pairs, err := e.CurrencyPairs.GetPairs(a, false)
		require.NoErrorf(t, err, "cannot get pairs for %s", a)
		require.NotEmptyf(t, pairs, "no pairs for %s", a)
		resp, err := e.GetCurrencyTradeURL(t.Context(), a, pairs[0])
		require.NoError(t, err)
		assert.NotEmpty(t, resp)
	}
}

// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
	p := currency.Pairs{testPairFiat}
	if e.Websocket.IsEnabled() && !e.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(e) {
		t.Skip(websocket.ErrWebsocketNotEnabled.Error())
	}
	var dialer gws.Dialer
	err := e.Websocket.Conn.Dial(t.Context(), &dialer, http.Header{})
	require.NoError(t, err)
	e.Websocket.Wg.Add(1)
	go e.wsReadData()
	err = e.Subscribe(subscription.List{
		{
			Channel:       "myAccount",
			Asset:         asset.All,
			Pairs:         p,
			Authenticated: true,
		},
	})
	assert.NoError(t, err)
	timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
	select {
	case badResponse := <-e.Websocket.DataHandler:
		assert.IsType(t, []order.Detail{}, badResponse)
	case <-timer.C:
	}
	timer.Stop()
}

func TestWsHandleData(t *testing.T) {
	done := make(chan struct{})
	t.Cleanup(func() {
		close(done)
	})
	go func() {
		for {
			select {
			case <-e.Websocket.DataHandler:
				continue
			case <-done:
				return
			}
		}
	}()
	_, err := e.wsHandleData(nil)
	var syntaxErr *json.SyntaxError
	assert.True(t, errors.As(err, &syntaxErr) || strings.Contains(err.Error(), "Syntax error no sources available, the input json is empty"), errJSONUnmarshalUnexpected)
	mockJSON := []byte(`{"type": "error"}`)
	_, err = e.wsHandleData(mockJSON)
	assert.Error(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "subscriptions"}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	var unmarshalTypeErr *json.UnmarshalTypeError
	mockJSON = []byte(`{"sequence_num": 0, "channel": "status", "events": [{"type": 1234}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "status", "events": [{"type": "moo"}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "ticker", "events": [{"type": "moo", "tickers": false}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "candles", "events": [{"type": false}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "candles", "events": [{"type": "moo", "candles": [{"low": "1.1"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "market_trades", "events": [{"type": false}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "market_trades", "events": [{"type": "moo", "trades": [{"price": "1.1"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "events": [{"type": false, "updates": [{"price_level": "1.1"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "moo", "updates": [{"price_level": "1.1"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.ErrorIs(t, err, errUnknownL2DataType)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "snapshot", "product_id": "BTC-USD", "updates": [{"side": "bid", "price_level": "1.1", "new_quantity": "2.2"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "l2_data", "timestamp": "2006-01-02T15:04:05Z", "events": [{"type": "update", "product_id": "BTC-USD", "updates": [{"side": "bid", "price_level": "1.1", "new_quantity": "2.2"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "user", "events": [{"type": false}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.True(t, errors.As(err, &unmarshalTypeErr) || strings.Contains(err.Error(), "mismatched type with value"), errJSONUnmarshalUnexpected)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "user", "events": [{"type": "moo", "orders": [{"limit_price": "2.2", "total_fees": "1.1", "post_only": true}], "positions": {"perpetual_futures_positions": [{"margin_type": "fakeMarginType"}], "expiring_futures_positions": [{}]}}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
	mockJSON = []byte(`{"sequence_num": 0, "channel": "fakechan", "events": [{"type": ""}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.ErrorIs(t, err, errChannelNameUnknown)
	p, err := e.FormatExchangeCurrency(currency.NewBTCUSD(), asset.Spot)
	require.NoError(t, err)
	e.pairAliases.Load(map[currency.Pair]currency.Pairs{
		p: {p},
	})
	mockJSON = []byte(`{"sequence_num": 0, "channel": "ticker", "events": [{"type": "moo", "tickers": [{"product_id": "BTC-USD", "price": "1.1"}]}]}`)
	_, err = e.wsHandleData(mockJSON)
	assert.NoError(t, err)
}

func TestProcessSnapshotUpdate(t *testing.T) {
	t.Parallel()
	req := WebsocketOrderbookDataHolder{Changes: []WebsocketOrderbookData{{Side: "fakeside", PriceLevel: 1.1, NewQuantity: 2.2}}, ProductID: currency.NewBTCUSD()}
	err := e.ProcessSnapshot(&req, time.Time{})
	assert.ErrorIs(t, err, order.ErrSideIsInvalid)
	err = e.ProcessUpdate(&req, time.Time{})
	assert.ErrorIs(t, err, order.ErrSideIsInvalid)
	req.Changes[0].Side = "offer"
	err = e.ProcessSnapshot(&req, time.Now())
	assert.NoError(t, err)
	err = e.ProcessUpdate(&req, time.Now())
	assert.NoError(t, err)
}

func TestGenerateSubscriptions(t *testing.T) {
	t.Parallel()
	e := new(Exchange)
	if err := testexch.Setup(e); err != nil {
		log.Fatal(err)
	}
	e.Websocket.SetCanUseAuthenticatedEndpoints(true)
	p1, err := e.GetEnabledPairs(asset.Spot)
	require.NoError(t, err)
	p2, err := e.GetEnabledPairs(asset.Futures)
	require.NoError(t, err)
	exp := subscription.List{}
	for _, baseSub := range defaultSubscriptions.Enabled() {
		s := baseSub.Clone()
		s.QualifiedChannel = subscriptionNames[s.Channel]
		switch s.Asset {
		case asset.Spot:
			s.Pairs = p1
		case asset.Futures:
			s.Pairs = p2
		case asset.All:
			s2 := s.Clone()
			s2.Asset = asset.Futures
			s2.Pairs = p2
			exp = append(exp, s2)
			s.Asset = asset.Spot
			s.Pairs = p1
		}
		exp = append(exp, s)
	}
	subs, err := e.generateSubscriptions()
	require.NoError(t, err)
	testsubs.EqualLists(t, exp, subs)
	_, err = subscription.List{{Channel: "wibble"}}.ExpandTemplates(e)
	assert.ErrorContains(t, err, "subscription channel not supported: wibble")
}

func TestSubscribeUnsubscribe(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	req := subscription.List{{Channel: "heartbeat", Asset: asset.Spot, Pairs: currency.Pairs{currency.NewPairWithDelimiter(testCrypto.String(), testFiat.String(), "-")}}}
	err := e.Subscribe(req)
	assert.NoError(t, err)
	err = e.Unsubscribe(req)
	assert.NoError(t, err)
}

func TestCheckSubscriptions(t *testing.T) {
	t.Parallel()
	e := &Exchange{
		Base: exchange.Base{
			Config: &config.Exchange{
				Features: &config.FeaturesConfig{
					Subscriptions: subscription.List{
						{Enabled: true, Channel: "matches"},
					},
				},
			},
			Features: exchange.Features{},
		},
	}
	e.checkSubscriptions()
	testsubs.EqualLists(t, defaultSubscriptions.Enabled(), e.Features.Subscriptions)
	testsubs.EqualLists(t, defaultSubscriptions, e.Config.Features.Subscriptions)
}

func TestGetJWT(t *testing.T) {
	t.Parallel()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	_, _, err := e.GetJWT(t.Context(), "a")
	assert.NoError(t, err)
}

func TestEncodeDateRange(t *testing.T) {
	t.Parallel()
	_, err := urlValsFromDateRange(time.Time{}, time.Time{}, "", "")
	assert.NoError(t, err)
	_, err = urlValsFromDateRange(time.Unix(1, 1), time.Unix(1, 1), "", "")
	assert.ErrorIs(t, err, common.ErrStartEqualsEnd)
	_, err = urlValsFromDateRange(time.Unix(1, 1), time.Unix(2, 2), "", "")
	assert.ErrorIs(t, err, errDateLabelEmpty)
	vals, err := urlValsFromDateRange(time.Unix(1, 1), time.Unix(2, 2), "start", "end")
	assert.NoError(t, err)
	assert.NotEmpty(t, vals)
}

func TestEncodePagination(t *testing.T) {
	t.Parallel()
	vals := urlValsFromPagination(PaginationInp{
		Limit:         1,
		OrderAscend:   true,
		StartingAfter: "a",
		EndingBefore:  "b",
	})
	assert.NotEmpty(t, vals)
}

func TestCreateOrderConfig(t *testing.T) {
	t.Parallel()
	_, err := createOrderConfig(nil)
	assert.ErrorIs(t, err, common.ErrNilPointer)
	params := &OrderInfo{}
	_, err = createOrderConfig(params)
	assert.ErrorIs(t, err, errInvalidOrderType)
	params.BaseAmount = 1
	params.QuoteAmount = 2
	params.OrderType = order.Market
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.OrderType = order.Limit
	params.TimeInForce = order.StopOrReduce
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.TimeInForce = order.FillOrKill
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.TimeInForce = order.UnknownTIF
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.EndTime = time.Unix(1, 1)
	_, err = createOrderConfig(params)
	assert.ErrorIs(t, err, errEndTimeInPast)
	params.EndTime = time.Now().Add(time.Hour)
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.OrderType = order.TWAP
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.EndTime = time.Time{}
	_, err = createOrderConfig(params)
	assert.ErrorIs(t, err, errEndTimeInPast)
	params.OrderType = order.StopLimit
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.EndTime = time.Unix(1, 1)
	_, err = createOrderConfig(params)
	assert.ErrorIs(t, err, errEndTimeInPast)
	params.EndTime = time.Now().Add(time.Hour)
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.OrderType = order.Bracket
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
	params.EndTime = time.Unix(1, 1)
	_, err = createOrderConfig(params)
	assert.ErrorIs(t, err, errEndTimeInPast)
	params.EndTime = time.Time{}
	_, err = createOrderConfig(params)
	assert.NoError(t, err)
}

func TestFormatMarginType(t *testing.T) {
	t.Parallel()
	resp := FormatMarginType("ISOLATED")
	assert.Equal(t, "ISOLATED", resp)
	resp = FormatMarginType("MULTI")
	assert.Equal(t, "CROSS", resp)
	resp = FormatMarginType("fake")
	assert.Empty(t, resp)
}

func TestStatusToStandardStatus(t *testing.T) {
	t.Parallel()
	resp, _ := statusToStandardStatus("PENDING")
	assert.Equal(t, order.New, resp)
	resp, _ = statusToStandardStatus("OPEN")
	assert.Equal(t, order.Active, resp)
	resp, _ = statusToStandardStatus("FILLED")
	assert.Equal(t, order.Filled, resp)
	resp, _ = statusToStandardStatus("CANCELLED")
	assert.Equal(t, order.Cancelled, resp)
	resp, _ = statusToStandardStatus("EXPIRED")
	assert.Equal(t, order.Expired, resp)
	resp, _ = statusToStandardStatus("FAILED")
	assert.Equal(t, order.Rejected, resp)
	_, err := statusToStandardStatus("")
	assert.ErrorIs(t, err, order.ErrUnsupportedStatusType)
}

func TestStringToStandardType(t *testing.T) {
	t.Parallel()
	resp, _ := stringToStandardType("LIMIT_ORDER_TYPE")
	assert.Equal(t, order.Limit, resp)
	resp, _ = stringToStandardType("MARKET_ORDER_TYPE")
	assert.Equal(t, order.Market, resp)
	resp, _ = stringToStandardType("STOP_LIMIT_ORDER_TYPE")
	assert.Equal(t, order.StopLimit, resp)
	_, err := stringToStandardType("")
	assert.ErrorIs(t, err, order.ErrUnrecognisedOrderType)
}

func TestStringToStandardAsset(t *testing.T) {
	t.Parallel()
	resp, _ := stringToStandardAsset("SPOT")
	assert.Equal(t, asset.Spot, resp)
	resp, _ = stringToStandardAsset("FUTURE")
	assert.Equal(t, asset.Futures, resp)
	_, err := stringToStandardAsset("")
	assert.ErrorIs(t, err, asset.ErrNotSupported)
}

func TestStrategyDecoder(t *testing.T) {
	t.Parallel()
	resp, _ := strategyDecoder("IMMEDIATE_OR_CANCEL")
	assert.True(t, resp.Is(order.ImmediateOrCancel))
	resp, _ = strategyDecoder("FILL_OR_KILL")
	assert.True(t, resp.Is(order.FillOrKill))
	resp, _ = strategyDecoder("GOOD_UNTIL_CANCELLED")
	assert.True(t, resp.Is(order.GoodTillCancel))
	resp, _ = strategyDecoder("GOOD_UNTIL_DATE_TIME")
	assert.True(t, resp.Is(order.GoodTillDay|order.GoodTillTime))
	_, err := strategyDecoder("")
	assert.ErrorIs(t, err, errUnrecognisedStrategyType)
}

func TestProcessFundingData(t *testing.T) {
	t.Parallel()
	accHistory := []DeposWithdrData{
		{
			Type: "unknown",
		},
		{
			Type: "TRANSFER_TYPE_WITHDRAWAL",
		},
	}
	cryptoHistory := []TransactionData{
		{
			Type: "receive",
		},
		{
			Type: "send",
		},
	}
	_, err := e.processFundingData(accHistory, cryptoHistory)
	assert.ErrorIs(t, err, errUnknownTransferType)
	accHistory[0].Type = "TRANSFER_TYPE_DEPOSIT"
	resp, err := e.processFundingData(accHistory, cryptoHistory)
	require.NoError(t, err)
	assert.NotEmpty(t, resp)
}

func TestChannelName(t *testing.T) {
	_, err := channelName(&subscription.Subscription{})
	assert.ErrorIs(t, err, subscription.ErrNotSupported)
	_, err = channelName(&subscription.Subscription{Channel: subscription.HeartbeatChannel})
	assert.NoError(t, err)
}

func exchangeBaseHelper(e *Exchange) error {
	if err := testexch.Setup(e); err != nil {
		return err
	}
	if apiKey != "" {
		e.SetCredentials(apiKey, apiSecret, "", "", "", "")
		e.API.AuthenticatedSupport = true
		e.API.AuthenticatedWebsocketSupport = true
	}
	return nil
}

func getINTXPortfolio(t *testing.T) string {
	t.Helper()
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	resp, err := e.GetAllPortfolios(t.Context(), "")
	assert.NoError(t, err)
	if len(resp) == 0 {
		t.Skip(skipInsufficientPortfolios)
	}
	var targetID string
	for i := range resp {
		if resp[i].Type == "INTX" {
			targetID = resp[i].UUID
			break
		}
	}
	if targetID == "" {
		t.Skip(skipInsufficientPortfolios)
	}
	return targetID
}

func convertTestHelper(t *testing.T) (fromAccID, toAccID string) {
	t.Helper()
	accIDs, err := e.ListAccounts(t.Context(), 250, 0)
	assert.NoError(t, err)
	if accIDs == nil || len(accIDs.Accounts) == 0 {
		t.Fatal(errExpectedNonEmpty)
	}
	for x := range accIDs.Accounts {
		if accIDs.Accounts[x].Currency == testStable {
			fromAccID = accIDs.Accounts[x].UUID
		}
		if accIDs.Accounts[x].Currency == testFiat {
			toAccID = accIDs.Accounts[x].UUID
		}
		if fromAccID != "" && toAccID != "" {
			break
		}
	}
	if fromAccID == "" || toAccID == "" {
		t.Skip(skipInsufSuitableAccs)
	}
	return fromAccID, toAccID
}

func transferTestHelper(t *testing.T, wallets *GetAllWalletsResponse) (srcWalletID, tarWalletID string) {
	t.Helper()
	var hasValidFunds bool
	for i := range wallets.Data {
		if wallets.Data[i].Currency.Code == testFiat.String() && wallets.Data[i].Balance.Amount > 10 {
			hasValidFunds = true
			srcWalletID = wallets.Data[i].ID
		}
	}
	if !hasValidFunds {
		t.Skip(skipInsufficientFunds)
	}
	pmID, err := e.ListPaymentMethods(t.Context())
	assert.NoError(t, err)
	if len(pmID) == 0 {
		t.Skip(skipPayMethodNotFound)
	}
	return srcWalletID, pmID[0].ID
}

type withdrawFiatFunc func(context.Context, *withdraw.Request) (*withdraw.ExchangeResponse, error)

func withdrawFiatFundsHelper(t *testing.T, fn withdrawFiatFunc) {
	t.Helper()
	t.Parallel()
	req := withdraw.Request{}
	_, err := fn(t.Context(), &req)
	assert.ErrorIs(t, err, common.ErrExchangeNameNotSet)
	req.Exchange = e.Name
	req.Currency = testFiat
	req.Amount = 1
	req.Type = withdraw.Fiat
	req.Fiat.Bank.Enabled = true
	req.Fiat.Bank.SupportedExchanges = "Coinbase"
	req.Fiat.Bank.SupportedCurrencies = testFiat.String()
	req.Fiat.Bank.AccountNumber = "123"
	req.Fiat.Bank.SWIFTCode = "456"
	req.Fiat.Bank.BSBNumber = "789"
	_, err = fn(t.Context(), &req)
	assert.ErrorIs(t, err, errWalletIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
	req.WalletID = "meow"
	req.Fiat.Bank.BankName = "GCT's Officially Fake and Not Real Test Bank"
	_, err = fn(t.Context(), &req)
	assert.ErrorIs(t, err, errPayMethodNotFound)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	wallets, err := e.GetAllWallets(t.Context(), PaginationInp{})
	assert.NoError(t, err)
	if wallets == nil || len(wallets.Data) == 0 {
		t.Fatal(errExpectedNonEmpty)
	}
	req.WalletID = ""
	for i := range wallets.Data {
		if wallets.Data[i].Currency.Name == testFiat.String() && wallets.Data[i].Balance.Amount > testAmount*100 {
			req.WalletID = wallets.Data[i].ID
			break
		}
	}
	if req.WalletID == "" {
		t.Skip(skipInsufficientFunds)
	}
	req.Fiat.Bank.BankName = "AUD Wallet"
	resp, err := fn(t.Context(), &req)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

type getNoArgsResp interface {
	*ServerTimeV3 | []PaymentMethodData | *UserResponse | []FiatData | []CryptoData | *ServerTimeV2 | []CurrencyData | []PairVolumeData | *AllWrappedAssets
}

type getNoArgsAssertNotEmpty[G getNoArgsResp] func(context.Context) (G, error)

func testGetNoArgs[G getNoArgsResp](t *testing.T, f getNoArgsAssertNotEmpty[G]) {
	t.Helper()
	resp, err := f(t.Context())
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

type genConvertTestFunc func(context.Context, string, string, string) (*ConvertResponse, error)

func convertTestShared(t *testing.T, f genConvertTestFunc) {
	t.Helper()
	t.Parallel()
	_, err := f(t.Context(), "", "", "")
	assert.ErrorIs(t, err, errTransactionIDEmpty)
	_, err = f(t.Context(), "meow", "", "")
	assert.ErrorIs(t, err, errAccountIDEmpty)
	sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
	fromAccID, toAccID := convertTestHelper(t)
	resp, err := e.CreateConvertQuote(t.Context(), fromAccID, toAccID, "", "", 0.01)
	require.NoError(t, err)
	require.NotNil(t, resp)
	resp, err = f(t.Context(), resp.ID, fromAccID, toAccID)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}

type getOneArgResp interface {
	*CurrencyData | *PairData | *ProductStats | *ProductTicker | *WrappedAsset | *WrappedAssetConversionRate
}

type getOneArgAssertNotEmpty[G getOneArgResp] func(context.Context, string) (G, error)

func testGetOneArg[G getOneArgResp](t *testing.T, f getOneArgAssertNotEmpty[G], arg string, tarErr error) {
	t.Helper()
	_, err := f(t.Context(), "")
	assert.ErrorIs(t, err, tarErr)
	resp, err := f(t.Context(), arg)
	require.NoError(t, err)
	assert.NotEmpty(t, resp, errExpectedNonEmpty)
}
