package bitget

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
	"github.com/c9s/bbgo/pkg/fixedpoint"
	"github.com/c9s/bbgo/pkg/testing/httptesting"
	"github.com/c9s/bbgo/pkg/types"
)

func TestExchange_QueryMarkets(t *testing.T) {
	ex := New("key", "secret", "passphrase")

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_symbols_request.json")
		assert.NoError(t, err)

		transport.GET("/api/v2/spot/public/symbols", func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		mkts, err := ex.QueryMarkets(context.Background())
		assert.NoError(t, err)

		expMkts := types.MarketMap{
			"ETHUSDT": types.Market{
				Exchange:        types.ExchangeBitget,
				Symbol:          "ETHUSDT",
				LocalSymbol:     "ETHUSDT",
				PricePrecision:  2,
				VolumePrecision: 4,
				QuoteCurrency:   "USDT",
				BaseCurrency:    "ETH",
				MinNotional:     fixedpoint.NewFromInt(5),
				MinAmount:       fixedpoint.NewFromInt(5),
				MinQuantity:     fixedpoint.NewFromInt(0),
				MaxQuantity:     fixedpoint.NewFromInt(10000000000),
				StepSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(4)),
				TickSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(2)),
				MinPrice:        fixedpoint.Zero,
				MaxPrice:        fixedpoint.Zero,
			},
			"BTCUSDT": types.Market{
				Exchange:        types.ExchangeBitget,
				Symbol:          "BTCUSDT",
				LocalSymbol:     "BTCUSDT",
				PricePrecision:  2,
				VolumePrecision: 6,
				QuoteCurrency:   "USDT",
				BaseCurrency:    "BTC",
				MinNotional:     fixedpoint.NewFromInt(5),
				MinAmount:       fixedpoint.NewFromInt(5),
				MinQuantity:     fixedpoint.NewFromInt(0),
				MaxQuantity:     fixedpoint.NewFromInt(10000000000),
				StepSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(6)),
				TickSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(2)),
				MinPrice:        fixedpoint.Zero,
				MaxPrice:        fixedpoint.Zero,
			},
		}
		assert.Equal(t, expMkts, mkts)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(t, err)

		transport.GET("/api/v2/spot/public/symbols", func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryMarkets(context.Background())
		assert.ErrorContains(t, err, "Invalid IP")
	})
}

func TestExchange_QueryTicker(t *testing.T) {
	var (
		assert = assert.New(t)
		ex     = New("key", "secret", "passphrase")
		url    = "/api/v2/spot/market/tickers"
	)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_ticker_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		tickers, err := ex.QueryTicker(context.Background(), "BTCUSDT")
		assert.NoError(err)
		expTicker := &types.Ticker{
			Time:   types.NewMillisecondTimestampFromInt(1709626631127).Time(),
			Volume: fixedpoint.MustNewFromString("29439.351448"),
			Last:   fixedpoint.MustNewFromString("66554.03"),
			Open:   fixedpoint.MustNewFromString("64654.54"),
			High:   fixedpoint.MustNewFromString("68686.93"),
			Low:    fixedpoint.MustNewFromString("64583.42"),
			Buy:    fixedpoint.MustNewFromString("66554"),
			Sell:   fixedpoint.MustNewFromString("66554.07"),
		}
		assert.Equal(expTicker, tickers)
	})

	t.Run("unexpected length", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_tickers_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		_, err = ex.QueryTicker(context.Background(), "BTCUSDT")
		assert.ErrorContains(err, "unexpected length of query")
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryTicker(context.Background(), "BTCUSDT")
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_QueryTickers(t *testing.T) {
	var (
		assert       = assert.New(t)
		ex           = New("key", "secret", "passphrase")
		url          = "/api/v2/spot/market/tickers"
		expBtcSymbol = "BTCUSDT"
		expBtcTicker = types.Ticker{
			Time:   types.NewMillisecondTimestampFromInt(1709626631127).Time(),
			Volume: fixedpoint.MustNewFromString("29439.351448"),
			Last:   fixedpoint.MustNewFromString("66554.03"),
			Open:   fixedpoint.MustNewFromString("64654.54"),
			High:   fixedpoint.MustNewFromString("68686.93"),
			Low:    fixedpoint.MustNewFromString("64583.42"),
			Buy:    fixedpoint.MustNewFromString("66554"),
			Sell:   fixedpoint.MustNewFromString("66554.07"),
		}
	)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_tickers_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		tickers, err := ex.QueryTickers(context.Background())
		assert.NoError(err)
		expTickers := map[string]types.Ticker{
			expBtcSymbol: expBtcTicker,
			"ETHUSDT": {
				Time:   types.NewMillisecondTimestampFromInt(1709626631726).Time(),
				Volume: fixedpoint.MustNewFromString("243220.866"),
				Last:   fixedpoint.MustNewFromString("3686.95"),
				Open:   fixedpoint.MustNewFromString("3506.6"),
				High:   fixedpoint.MustNewFromString("3740"),
				Low:    fixedpoint.MustNewFromString("3461.17"),
				Buy:    fixedpoint.MustNewFromString("3686.94"),
				Sell:   fixedpoint.MustNewFromString("3686.98"),
			},
		}
		assert.Equal(expTickers, tickers)
	})

	t.Run("succeeds for query one markets", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_ticker_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			assert.Contains(req.URL.Query(), "symbol")
			assert.Equal(req.URL.Query()["symbol"], []string{expBtcSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		tickers, err := ex.QueryTickers(context.Background(), expBtcSymbol)
		assert.NoError(err)
		expTickers := map[string]types.Ticker{
			expBtcSymbol: expBtcTicker,
		}
		assert.Equal(expTickers, tickers)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryTicker(context.Background(), expBtcSymbol)
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_QueryKLines(t *testing.T) {
	var (
		assert       = assert.New(t)
		ex           = New("key", "secret", "passphrase")
		url          = "/api/v2/spot/market/candles"
		expBtcSymbol = "BTCUSDT"
		interval     = types.Interval4h
		expBtcKlines = []types.KLine{
			{
				Exchange:    types.ExchangeBitget,
				Symbol:      expBtcSymbol,
				StartTime:   types.Time(types.NewMillisecondTimestampFromInt(1709352000000).Time()),
				EndTime:     types.Time(types.NewMillisecondTimestampFromInt(1709352000000).Time().Add(interval.Duration() - time.Millisecond)),
				Interval:    interval,
				Open:        fixedpoint.MustNewFromString("62308.42"),
				Close:       fixedpoint.MustNewFromString("62014.17"),
				High:        fixedpoint.MustNewFromString("62308.43"),
				Low:         fixedpoint.MustNewFromString("61760"),
				Volume:      fixedpoint.MustNewFromString("987.377637"),
				QuoteVolume: fixedpoint.MustNewFromString("61283110.57046518"),
				Closed:      false,
			},
			{
				Exchange:    types.ExchangeBitget,
				Symbol:      expBtcSymbol,
				StartTime:   types.Time(types.NewMillisecondTimestampFromInt(1709366400000).Time()),
				EndTime:     types.Time(types.NewMillisecondTimestampFromInt(1709366400000).Time().Add(interval.Duration() - time.Millisecond)),
				Interval:    interval,
				Open:        fixedpoint.MustNewFromString("62014.17"),
				Close:       fixedpoint.MustNewFromString("61825.64"),
				High:        fixedpoint.MustNewFromString("62122.8"),
				Low:         fixedpoint.MustNewFromString("61648.26"),
				Volume:      fixedpoint.MustNewFromString("1271.183413"),
				QuoteVolume: fixedpoint.MustNewFromString("78680550.55539777"),
				Closed:      false,
			},
		}
	)

	t.Run("succeeds without time range", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_k_line_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 3)
			assert.Contains(query, "symbol")
			assert.Contains(query, "granularity")
			assert.Contains(query, "limit")
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			assert.Equal(query["granularity"], []string{interval.String()})
			assert.Equal(query["limit"], []string{strconv.Itoa(defaultKLineLimit)})
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		klines, err := ex.QueryKLines(context.Background(), expBtcSymbol, interval, types.KLineQueryOptions{})
		assert.NoError(err)
		assert.Equal(expBtcKlines, klines)
	})

	t.Run("succeeds with time range", func(t *testing.T) {
		var (
			transport   = &httptesting.MockTransport{}
			limit       = 50
			startTime   = time.Now()
			endTime     = startTime.Add(8 * time.Hour)
			startTimeMs = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10)
			endTimeMs   = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
		)

		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_k_line_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 5)
			assert.Contains(query, "symbol")
			assert.Contains(query, "granularity")
			assert.Contains(query, "limit")
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			assert.Equal(query["granularity"], []string{interval.String()})
			assert.Equal(query["limit"], []string{strconv.Itoa(limit)})
			assert.Equal(query["startTime"], []string{startTimeMs})
			assert.Equal(query["endTime"], []string{endTimeMs})

			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		klines, err := ex.QueryKLines(context.Background(), expBtcSymbol, interval, types.KLineQueryOptions{
			Limit:     limit,
			StartTime: &startTime,
			EndTime:   &endTime,
		})
		assert.NoError(err)
		assert.Equal(expBtcKlines, klines)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryKLines(context.Background(), expBtcSymbol, interval, types.KLineQueryOptions{})
		assert.ErrorContains(err, "Invalid IP")
	})

	t.Run("reach max duration", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponse(http.StatusBadRequest, nil), nil
		})

		before31Days := time.Now().Add(-31 * 24 * time.Hour)
		_, err := ex.QueryKLines(context.Background(), expBtcSymbol, types.Interval1m, types.KLineQueryOptions{
			EndTime: &before31Days,
		})
		assert.ErrorContains(err, "are greater than max duration")
	})

	t.Run("end time before start time", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponse(http.StatusBadRequest, nil), nil
		})

		startTime := time.Now()
		endTime := startTime.Add(-time.Hour)
		_, err := ex.QueryKLines(context.Background(), expBtcSymbol, types.Interval1m, types.KLineQueryOptions{
			StartTime: &startTime,
			EndTime:   &endTime,
		})
		assert.ErrorContains(err, "before start time")
	})

	t.Run("unexpected duraiton", func(t *testing.T) {
		_, err := ex.QueryKLines(context.Background(), expBtcSymbol, "87h", types.KLineQueryOptions{})
		assert.ErrorContains(err, "not supported")
	})
}

func TestExchange_QueryAccount(t *testing.T) {
	var (
		assert     = assert.New(t)
		ex         = New("key", "secret", "passphrase")
		url        = "/api/v2/spot/account/assets"
		expAccount = &types.Account{
			AccountType: "spot",
			FuturesInfo: &types.FuturesAccount{
				Assets:    types.FuturesAssetMap{},
				Positions: types.FuturesPositionMap{},
			},
			MarginInfo:         nil,
			IsolatedMarginInfo: nil,
			MarginLevel:        fixedpoint.Zero,
			MarginTolerance:    fixedpoint.Zero,
			BorrowEnabled:      types.BoolPtr(false),
			TransferEnabled:    types.BoolPtr(false),
			MarginRatio:        fixedpoint.Zero,
			LiquidationPrice:   fixedpoint.Zero,
			LiquidationRate:    fixedpoint.Zero,
			MakerFeeRate:       fixedpoint.Zero,
			TakerFeeRate:       fixedpoint.Zero,
			TotalAccountValue:  fixedpoint.Zero,
			CanDeposit:         false,
			CanTrade:           false,
			CanWithdraw:        false,
		}
		balances = types.BalanceMap{
			"BTC": {
				Currency:          "BTC",
				Available:         fixedpoint.MustNewFromString("0.00000690"),
				Locked:            fixedpoint.Zero,
				Borrowed:          fixedpoint.Zero,
				Interest:          fixedpoint.Zero,
				NetAsset:          fixedpoint.Zero,
				MaxWithdrawAmount: fixedpoint.Zero,
			},
			"USDT": {
				Currency:          "USDT",
				Available:         fixedpoint.MustNewFromString("0.68360342"),
				Locked:            fixedpoint.MustNewFromString("9.08096000"),
				Borrowed:          fixedpoint.Zero,
				Interest:          fixedpoint.Zero,
				NetAsset:          fixedpoint.Zero,
				MaxWithdrawAmount: fixedpoint.Zero,
			},
		}
	)
	expAccount.UpdateBalances(balances)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_account_assets_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 1)
			assert.Contains(query, "limit")
			assert.Equal(query["limit"], []string{string(v2.AssetTypeHoldOnly)})
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		acct, err := ex.QueryAccount(context.Background())
		assert.NoError(err)
		assert.Equal(expAccount, acct)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryAccount(context.Background())
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_QueryAccountBalances(t *testing.T) {
	var (
		assert         = assert.New(t)
		ex             = New("key", "secret", "passphrase")
		url            = "/api/v2/spot/account/assets"
		expBalancesMap = types.BalanceMap{
			"BTC": {
				Currency:          "BTC",
				Available:         fixedpoint.MustNewFromString("0.00000690"),
				Locked:            fixedpoint.Zero,
				Borrowed:          fixedpoint.Zero,
				Interest:          fixedpoint.Zero,
				NetAsset:          fixedpoint.Zero,
				MaxWithdrawAmount: fixedpoint.Zero,
			},
			"USDT": {
				Currency:          "USDT",
				Available:         fixedpoint.MustNewFromString("0.68360342"),
				Locked:            fixedpoint.MustNewFromString("9.08096000"),
				Borrowed:          fixedpoint.Zero,
				Interest:          fixedpoint.Zero,
				NetAsset:          fixedpoint.Zero,
				MaxWithdrawAmount: fixedpoint.Zero,
			},
		}
	)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_account_assets_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 1)
			assert.Contains(query, "limit")
			assert.Equal(query["limit"], []string{string(v2.AssetTypeHoldOnly)})
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		acct, err := ex.QueryAccountBalances(context.Background())
		assert.NoError(err)
		assert.Equal(expBalancesMap, acct)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryAccountBalances(context.Background())
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_SubmitOrder(t *testing.T) {
	var (
		assert        = assert.New(t)
		ex            = New("key", "secret", "passphrase")
		placeOrderUrl = "/api/v2/spot/trade/place-order"
		tickerUrl     = "/api/v2/spot/market/tickers"
		clientOrderId = "684a79df-f931-474f-a9a5-f1deab1cd770"
		expBtcSymbol  = "BTCUSDT"
		mkt           = types.Market{
			Symbol:          expBtcSymbol,
			LocalSymbol:     expBtcSymbol,
			PricePrecision:  fixedpoint.MustNewFromString("2").Int(),
			VolumePrecision: fixedpoint.MustNewFromString("6").Int(),
			StepSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(6)),
			TickSize:        fixedpoint.NewFromFloat(1.0 / math.Pow10(2)),
		}
		expOrder = &types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: clientOrderId,
				Symbol:        expBtcSymbol,
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeLimit,
				Quantity:      fixedpoint.MustNewFromString("0.00009"),
				Price:         fixedpoint.MustNewFromString("66000"),
				TimeInForce:   types.TimeInForceGTC,
				Market:        mkt,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          1148903850645331968,
			Status:           types.OrderStatusNew,
			ExecutedQuantity: fixedpoint.Zero,
			IsWorking:        true,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
		}
		reqLimitOrder = types.SubmitOrder{
			ClientOrderID: clientOrderId,
			Symbol:        expBtcSymbol,
			Side:          types.SideTypeBuy,
			Type:          types.OrderTypeLimit,
			Quantity:      fixedpoint.MustNewFromString("0.00009"),
			Price:         fixedpoint.MustNewFromString("66000"),
			Market:        mkt,
			TimeInForce:   types.TimeInForceGTC,
		}
	)

	type NewOrder struct {
		ClientOid string `json:"clientOid"`
		Force     string `json:"force"`
		OrderType string `json:"orderType"`
		Price     string `json:"price"`
		Side      string `json:"side"`
		Size      string `json:"size"`
		Symbol    string `json:"symbol"`
	}

	t.Run("Limit order", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
		assert.NoError(err)

		transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			reqq := &NewOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)
			assert.Equal(&NewOrder{
				ClientOid: expOrder.ClientOrderID,
				Force:     string(v2.OrderForceGTC),
				OrderType: string(v2.OrderTypeLimit),
				Price:     "66000.00",
				Side:      string(v2.SideTypeBuy),
				Size:      "0.000090",
				Symbol:    expBtcSymbol,
			}, reqq)

			return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
		})

		acct, err := ex.SubmitOrder(context.Background(), reqLimitOrder)
		assert.NoError(err)
		expOrder.CreationTime = acct.CreationTime
		expOrder.UpdateTime = acct.UpdateTime
		assert.Equal(expOrder, acct)
	})

	t.Run("Limit Maker order", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
		assert.NoError(err)

		transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			reqq := &NewOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)
			assert.Equal(&NewOrder{
				ClientOid: expOrder.ClientOrderID,
				Force:     string(v2.OrderForcePostOnly),
				OrderType: string(v2.OrderTypeLimit),
				Price:     "66000.00",
				Side:      string(v2.SideTypeBuy),
				Size:      "0.000090",
				Symbol:    expBtcSymbol,
			}, reqq)

			return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
		})

		reqLimitOrder2 := reqLimitOrder
		reqLimitOrder2.Type = types.OrderTypeLimitMaker
		acct, err := ex.SubmitOrder(context.Background(), reqLimitOrder2)
		assert.NoError(err)

		expOrder2 := *expOrder
		expOrder2.Status = types.OrderStatusNew
		expOrder2.IsWorking = true
		expOrder2.Type = types.OrderTypeLimitMaker
		expOrder2.SubmitOrder = reqLimitOrder2
		acct.CreationTime = expOrder2.CreationTime
		acct.UpdateTime = expOrder2.UpdateTime
		assert.Equal(&expOrder2, acct)
	})

	t.Run("Market order", func(t *testing.T) {
		t.Run("Buy", func(t *testing.T) {
			transport := &httptesting.MockTransport{}
			ex.client.HttpClient.Transport = transport

			// get ticker to calculate btc amount
			tickerFile, err := os.ReadFile("bitgetapi/v2/testdata/get_ticker_request.json")
			assert.NoError(err)

			transport.GET(tickerUrl, func(req *http.Request) (*http.Response, error) {
				assert.Contains(req.URL.Query(), "symbol")
				assert.Equal(req.URL.Query()["symbol"], []string{expBtcSymbol})
				return httptesting.BuildResponseString(http.StatusOK, string(tickerFile)), nil
			})

			// place order
			placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
			assert.NoError(err)

			transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
				raw, err := io.ReadAll(req.Body)
				assert.NoError(err)

				reqq := &NewOrder{}
				err = json.Unmarshal(raw, &reqq)
				assert.NoError(err)
				assert.Equal(&NewOrder{
					ClientOid: expOrder.ClientOrderID,
					Force:     string(v2.OrderForceGTC),
					OrderType: string(v2.OrderTypeMarket),
					Price:     "",
					Side:      string(v2.SideTypeBuy),
					Size:      reqLimitOrder.Market.FormatQuantity(fixedpoint.MustNewFromString("66554").Mul(fixedpoint.MustNewFromString("0.00009"))), // ticker: 66554, size: 0.00009
					Symbol:    expBtcSymbol,
				}, reqq)

				return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
			})

			reqMarketOrder := reqLimitOrder
			reqMarketOrder.Side = types.SideTypeBuy
			reqMarketOrder.Type = types.OrderTypeMarket
			acct, err := ex.SubmitOrder(context.Background(), reqMarketOrder)
			assert.NoError(err)
			expOrder2 := *expOrder
			expOrder2.Side = types.SideTypeBuy
			expOrder2.Type = types.OrderTypeMarket
			expOrder2.CreationTime = acct.CreationTime
			expOrder2.UpdateTime = acct.UpdateTime
			assert.Equal(&expOrder2, acct)
		})

		t.Run("Sell", func(t *testing.T) {
			transport := &httptesting.MockTransport{}
			ex.client.HttpClient.Transport = transport

			// get ticker to calculate btc amount
			tickerFile, err := os.ReadFile("bitgetapi/v2/testdata/get_ticker_request.json")
			assert.NoError(err)

			transport.GET(tickerUrl, func(req *http.Request) (*http.Response, error) {
				assert.Contains(req.URL.Query(), "symbol")
				assert.Equal(req.URL.Query()["symbol"], []string{expBtcSymbol})
				return httptesting.BuildResponseString(http.StatusOK, string(tickerFile)), nil
			})

			// place order
			placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
			assert.NoError(err)

			transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
				raw, err := io.ReadAll(req.Body)
				assert.NoError(err)

				reqq := &NewOrder{}
				err = json.Unmarshal(raw, &reqq)
				assert.NoError(err)
				assert.Equal(&NewOrder{
					ClientOid: expOrder.ClientOrderID,
					Force:     string(v2.OrderForceGTC),
					OrderType: string(v2.OrderTypeMarket),
					Price:     "",
					Side:      string(v2.SideTypeSell),
					Size:      "0.000090", // size: 0.00009
					Symbol:    expBtcSymbol,
				}, reqq)

				return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
			})

			reqMarketOrder := reqLimitOrder
			reqMarketOrder.Side = types.SideTypeSell
			reqMarketOrder.Type = types.OrderTypeMarket
			acct, err := ex.SubmitOrder(context.Background(), reqMarketOrder)
			assert.NoError(err)
			expOrder2 := *expOrder
			expOrder2.Side = types.SideTypeSell
			expOrder2.Type = types.OrderTypeMarket
			expOrder2.CreationTime = acct.CreationTime
			expOrder2.UpdateTime = acct.UpdateTime
			assert.Equal(&expOrder2, acct)
		})

		t.Run("failed to get ticker on buy", func(t *testing.T) {
			transport := &httptesting.MockTransport{}
			ex.client.HttpClient.Transport = transport

			// get ticker to calculate btc amount
			requestErrFile, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
			assert.NoError(err)

			transport.GET(tickerUrl, func(req *http.Request) (*http.Response, error) {
				assert.Contains(req.URL.Query(), "symbol")
				assert.Equal(req.URL.Query()["symbol"], []string{expBtcSymbol})
				return httptesting.BuildResponseString(http.StatusBadRequest, string(requestErrFile)), nil
			})

			reqMarketOrder := reqLimitOrder
			reqMarketOrder.Side = types.SideTypeBuy
			reqMarketOrder.Type = types.OrderTypeMarket
			_, err = ex.SubmitOrder(context.Background(), reqMarketOrder)
			assert.ErrorContains(err, "Invalid IP")
		})
	})

	t.Run("unexpected client order id", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
		assert.NoError(err)

		transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			reqq := &NewOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)
			assert.Equal(&NewOrder{
				ClientOid: expOrder.ClientOrderID,
				Force:     string(v2.OrderForceGTC),
				OrderType: string(v2.OrderTypeLimit),
				Price:     "66000.00",
				Side:      string(v2.SideTypeBuy),
				Size:      "0.000090",
				Symbol:    expBtcSymbol,
			}, reqq)

			apiResp := &v2.APIResponse{}
			err = json.Unmarshal(placeOrderFile, &apiResp)
			assert.NoError(err)
			placeOrderResp := &v2.PlaceOrderResponse{}
			err = json.Unmarshal(apiResp.Data, &placeOrderResp)
			assert.NoError(err)
			// remove the client order id to test
			placeOrderResp.ClientOrderId = "unexpected client order id"

			raw, err = json.Marshal(placeOrderResp)
			assert.NoError(err)
			apiResp.Data = raw
			raw, err = json.Marshal(apiResp)
			assert.NoError(err)

			return httptesting.BuildResponseString(http.StatusOK, string(raw)), nil
		})

		_, err = ex.SubmitOrder(context.Background(), reqLimitOrder)
		assert.ErrorContains(err, "unexpected order id")
	})

	t.Run("failed to place order", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
		assert.NoError(err)

		transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			reqq := &NewOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)
			assert.Equal(&NewOrder{
				ClientOid: expOrder.ClientOrderID,
				Force:     string(v2.OrderForceGTC),
				OrderType: string(v2.OrderTypeLimit),
				Price:     "66000.00",
				Side:      string(v2.SideTypeBuy),
				Size:      "0.000090",
				Symbol:    expBtcSymbol,
			}, reqq)

			return httptesting.BuildResponseString(http.StatusBadRequest, string(placeOrderFile)), nil
		})

		_, err = ex.SubmitOrder(context.Background(), reqLimitOrder)
		assert.ErrorContains(err, "failed to place order")
	})

	t.Run("unexpected client order id", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		reqOrder2 := reqLimitOrder
		reqOrder2.ClientOrderID = strings.Repeat("s", maxOrderIdLen+1)
		_, err := ex.SubmitOrder(context.Background(), reqOrder2)
		assert.ErrorContains(err, "unexpected length of client order id")
	})

	t.Run("unexpected side", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		reqOrder2 := reqLimitOrder
		reqOrder2.Side = "GG"
		_, err := ex.SubmitOrder(context.Background(), reqOrder2)
		assert.ErrorContains(err, "not supported")
	})

	t.Run("unexpected side", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		reqOrder2 := reqLimitOrder
		reqOrder2.Type = "GG"
		_, err := ex.SubmitOrder(context.Background(), reqOrder2)
		assert.ErrorContains(err, "not supported")
	})
}

func TestExchange_QueryOpenOrders(t *testing.T) {
	var (
		assert       = assert.New(t)
		ex           = New("key", "secret", "passphrase")
		expBtcSymbol = "BTCUSDT"
		url          = "/api/v2/spot/trade/unfilled-orders"
	)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/get_unfilled_orders_request_limit_order.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 2)
			assert.Contains(query, "symbol")
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			return httptesting.BuildResponseString(http.StatusOK, string(f)), nil
		})

		orders, err := ex.QueryOpenOrders(context.Background(), expBtcSymbol)
		assert.NoError(err)
		expOrder := []types.Order{
			{
				SubmitOrder: types.SubmitOrder{
					ClientOrderID: "684a79df-f931-474f-a9a5-f1deab1cd770",
					Symbol:        expBtcSymbol,
					Side:          types.SideTypeBuy,
					Type:          types.OrderTypeLimit,
					Quantity:      fixedpoint.MustNewFromString("0.00009"),
					Price:         fixedpoint.MustNewFromString("66000"),
					TimeInForce:   types.TimeInForceGTC,
				},
				Exchange:         types.ExchangeBitget,
				OrderID:          1148903850645331968,
				Status:           types.OrderStatusNew,
				ExecutedQuantity: fixedpoint.Zero,
				IsWorking:        true,
				CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
				UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
			},
		}
		assert.Equal(expOrder, orders)
	})

	t.Run("succeeds on pagination with mock limit + 1", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		dataTemplate := `{
      "userId":"8672173294",
      "symbol":"BTCUSDT",
      "orderId":"%d",
      "clientOid":"684a79df-f931-474f-a9a5-f1deab1cd770",
      "priceAvg":"0",
      "size":"0.00009",
      "orderType":"market",
      "side":"sell",
      "status":"live",
      "basePrice":"0",
      "baseVolume":"0",
      "quoteVolume":"0",
      "enterPointSource":"API",
      "orderSource":"market",
      "cTime":"1709645944272",
      "uTime":"1709645944272"
    }`

		openOrdersStr := make([]string, 0, queryLimit+1)
		expOrders := make([]types.Order, 0, queryLimit+1)
		for i := 0; i < queryLimit+1; i++ {
			dataStr := fmt.Sprintf(dataTemplate, i)
			openOrdersStr = append(openOrdersStr, dataStr)

			unfilledOdr := &v2.UnfilledOrder{}
			err := json.Unmarshal([]byte(dataStr), &unfilledOdr)
			assert.NoError(err)
			gOdr, err := unfilledOrderToGlobalOrder(*unfilledOdr)
			assert.NoError(err)
			expOrders = append(expOrders, *gOdr)
		}

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Contains(query, "symbol")
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			if len(query) == 2 {
				// first time query
				resp := &v2.APIResponse{
					Code: "00000",
					Data: []byte("[" + strings.Join(openOrdersStr[0:queryLimit], ",") + "]"),
				}
				respRaw, err := json.Marshal(resp)
				assert.NoError(err)

				return httptesting.BuildResponseString(http.StatusOK, string(respRaw)), nil
			}
			// second time query
			// last order id, so need to -1
			assert.Equal(query["idLessThan"], []string{strconv.FormatInt(queryLimit-1, 10)})

			resp := &v2.APIResponse{
				Code: "00000",
				Data: []byte("[" + strings.Join(openOrdersStr[queryLimit:queryLimit+1], ",") + "]"),
			}
			respRaw, err := json.Marshal(resp)
			assert.NoError(err)
			return httptesting.BuildResponseString(http.StatusOK, string(respRaw)), nil
		})

		orders, err := ex.QueryOpenOrders(context.Background(), expBtcSymbol)
		assert.NoError(err)
		assert.Equal(expOrders, orders)
	})

	t.Run("succeeds on pagination with mock limit + 1", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		dataTemplate := `{
      "userId":"8672173294",
      "symbol":"BTCUSDT",
      "orderId":"%d",
      "clientOid":"684a79df-f931-474f-a9a5-f1deab1cd770",
      "priceAvg":"0",
      "size":"0.00009",
      "orderType":"market",
      "side":"sell",
      "status":"live",
      "basePrice":"0",
      "baseVolume":"0",
      "quoteVolume":"0",
      "enterPointSource":"API",
      "orderSource":"market",
      "cTime":"1709645944272",
      "uTime":"1709645944272"
    }`

		openOrdersStr := make([]string, 0, queryLimit+1)
		for i := 0; i < queryLimit+1; i++ {
			dataStr := fmt.Sprintf(dataTemplate, i)
			openOrdersStr = append(openOrdersStr, dataStr)
		}

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Contains(query, "symbol")
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			// first time query
			resp := &v2.APIResponse{
				Code: "00000",
				Data: []byte("[" + strings.Join(openOrdersStr, ",") + "]"),
			}
			respRaw, err := json.Marshal(resp)
			assert.NoError(err)

			return httptesting.BuildResponseString(http.StatusOK, string(respRaw)), nil
		})

		_, err := ex.QueryOpenOrders(context.Background(), expBtcSymbol)
		assert.ErrorContains(err, "unexpected open orders length")
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryOpenOrders(context.Background(), "BTCUSDT")
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_QueryClosedOrders(t *testing.T) {
	var (
		assert       = assert.New(t)
		ex           = New("key", "secret", "passphrase")
		expBtcSymbol = "BTCUSDT"
		since        = time.Now().Add(-24 * time.Hour)
		until        = since.Add(time.Hour)
		lastOrderId  = uint64(0)
		url          = "/api/v2/spot/trade/history-orders"
	)

	t.Run("succeeds on market buy", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_history_orders_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 4)
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Contains(query, "limit")
			assert.Contains(query, "symbol")
			assert.Equal(query["startTime"], []string{strconv.FormatInt(since.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["endTime"], []string{strconv.FormatInt(until.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		orders, err := ex.QueryClosedOrders(context.Background(), expBtcSymbol, since, until, lastOrderId)
		assert.NoError(err)
		expOrder := []types.Order{
			{
				SubmitOrder: types.SubmitOrder{
					ClientOrderID: "684a79df-f931-474f-a9a5-f1deab1cd770",
					Symbol:        expBtcSymbol,
					Side:          types.SideTypeBuy,
					Type:          types.OrderTypeLimit,
					Quantity:      fixedpoint.MustNewFromString("0.00009"),
					Price:         fixedpoint.MustNewFromString("66000"),
					TimeInForce:   types.TimeInForceGTC,
				},
				Exchange:         types.ExchangeBitget,
				OrderID:          1148903850645331968,
				Status:           types.OrderStatusCanceled,
				ExecutedQuantity: fixedpoint.MustNewFromString("0"),
				IsWorking:        false,
				CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
				UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709648518713).Time()),
			},
			{
				SubmitOrder: types.SubmitOrder{
					ClientOrderID: "bf3ba805-66bc-4ef6-bf34-d63d79dc2e4c",
					Symbol:        expBtcSymbol,
					Side:          types.SideTypeBuy,
					Type:          types.OrderTypeMarket,
					Quantity:      fixedpoint.MustNewFromString("0.000089"),
					Price:         fixedpoint.MustNewFromString("67360.87"),
					TimeInForce:   types.TimeInForceGTC,
				},
				Exchange:         types.ExchangeBitget,
				OrderID:          1148914989018062853,
				Status:           types.OrderStatusFilled,
				ExecutedQuantity: fixedpoint.MustNewFromString("0.000089"),
				IsWorking:        false,
				CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709648599867).Time()),
				UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709648600016).Time()),
			},
		}
		assert.Equal(expOrder, orders)
	})

	t.Run("adjust time range since unexpected since and until", func(t *testing.T) {
		timeNow := time.Now().Truncate(time.Second)
		ex.timeNowFn = func() time.Time {
			return timeNow
		}
		defer func() { ex.timeNowFn = func() time.Time { return time.Now() } }()
		newSince := timeNow.Add(-maxHistoricalDataQueryPeriod * 2)
		expNewSince := timeNow.Add(-maxHistoricalDataQueryPeriod)
		newUntil := timeNow.Add(-maxHistoricalDataQueryPeriod)
		expNewUntil := timeNow

		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_history_orders_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 4)
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Contains(query, "limit")
			assert.Contains(query, "symbol")
			assert.Equal(query["startTime"], []string{strconv.FormatInt(expNewSince.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["endTime"], []string{strconv.FormatInt(expNewUntil.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			assert.Equal(query["symbol"], []string{expBtcSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		orders, err := ex.QueryClosedOrders(context.Background(), expBtcSymbol, newSince, newUntil, lastOrderId)
		assert.NoError(err)
		expOrder := []types.Order{
			{
				SubmitOrder: types.SubmitOrder{
					ClientOrderID: "684a79df-f931-474f-a9a5-f1deab1cd770",
					Symbol:        expBtcSymbol,
					Side:          types.SideTypeBuy,
					Type:          types.OrderTypeLimit,
					Quantity:      fixedpoint.MustNewFromString("0.00009"),
					Price:         fixedpoint.MustNewFromString("66000"),
					TimeInForce:   types.TimeInForceGTC,
				},
				Exchange:         types.ExchangeBitget,
				OrderID:          1148903850645331968,
				Status:           types.OrderStatusCanceled,
				ExecutedQuantity: fixedpoint.MustNewFromString("0"),
				IsWorking:        false,
				CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709645944272).Time()),
				UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709648518713).Time()),
			},
			{
				SubmitOrder: types.SubmitOrder{
					ClientOrderID: "bf3ba805-66bc-4ef6-bf34-d63d79dc2e4c",
					Symbol:        expBtcSymbol,
					Side:          types.SideTypeBuy,
					Type:          types.OrderTypeMarket,
					Quantity:      fixedpoint.MustNewFromString("0.000089"),
					Price:         fixedpoint.MustNewFromString("67360.87"),
					TimeInForce:   types.TimeInForceGTC,
				},
				Exchange:         types.ExchangeBitget,
				OrderID:          1148914989018062853,
				Status:           types.OrderStatusFilled,
				ExecutedQuantity: fixedpoint.MustNewFromString("0.000089"),
				IsWorking:        false,
				CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1709648599867).Time()),
				UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1709648600016).Time()),
			},
		}
		assert.Equal(expOrder, orders)
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryClosedOrders(context.Background(), expBtcSymbol, since, until, lastOrderId)
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_CancelOrders(t *testing.T) {
	var (
		assert         = assert.New(t)
		ex             = New("key", "secret", "passphrase")
		cancelOrderUrl = "/api/v2/spot/trade/cancel-order"
		order          = types.Order{
			OrderID: 1149899973610643488,
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "9471cf38-33c2-4aee-a2fb-fcf71629ffb7",
			},
		}
	)

	t.Run("order id first, either orderId or clientOid is required", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/cancel_order_request.json")
		assert.NoError(err)

		transport.POST(cancelOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			type cancelOrder struct {
				OrderId       string `json:"orderId"`
				ClientOrderId string `json:"clientOrderId"`
			}
			reqq := &cancelOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)

			assert.Equal(strconv.FormatUint(order.OrderID, 10), reqq.OrderId)
			assert.Equal("", reqq.ClientOrderId)
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		err = ex.CancelOrders(context.Background(), order)
		assert.NoError(err)
	})

	t.Run("unexpected order id and client order id", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		transport.POST(cancelOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			type cancelOrder struct {
				OrderId       string `json:"orderId"`
				ClientOrderId string `json:"clientOrderId"`
			}
			reqq := &cancelOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)

			assert.Equal(strconv.FormatUint(order.OrderID, 10), reqq.OrderId)
			assert.Equal("", reqq.ClientOrderId)

			reqq.OrderId = "123456789"
			reqq.ClientOrderId = "test wrong order client id"
			raw, err = json.Marshal(reqq)
			assert.NoError(err)
			apiResp := v2.APIResponse{Code: "00000", Data: raw}
			raw, err = json.Marshal(apiResp)
			assert.NoError(err)
			return httptesting.BuildResponseString(http.StatusOK, string(raw)), nil
		})

		err := ex.CancelOrders(context.Background(), order)
		assert.ErrorContains(err, "order id mismatch")
	})

	t.Run("client order id", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/cancel_order_request.json")
		assert.NoError(err)

		transport.POST(cancelOrderUrl, func(req *http.Request) (*http.Response, error) {
			raw, err := io.ReadAll(req.Body)
			assert.NoError(err)

			type cancelOrder struct {
				OrderId       string `json:"orderId"`
				ClientOrderId string `json:"clientOid"`
			}
			reqq := &cancelOrder{}
			err = json.Unmarshal(raw, &reqq)
			assert.NoError(err)

			assert.Equal("", reqq.OrderId)
			assert.Equal(order.ClientOrderID, reqq.ClientOrderId)
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		newOrder := order
		newOrder.OrderID = 0
		err = ex.CancelOrders(context.Background(), newOrder)
		assert.NoError(err)
	})

	t.Run("empty order id and client order id", func(t *testing.T) {
		newOrder := order
		newOrder.OrderID = 0
		newOrder.ClientOrderID = ""
		err := ex.CancelOrders(context.Background(), newOrder)
		assert.ErrorContains(err, "the order uuid and client order id are empty")
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.POST(cancelOrderUrl, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		err = ex.CancelOrders(context.Background(), order)
		assert.ErrorContains(err, "Invalid IP")
	})
}

func TestExchange_QueryTrades(t *testing.T) {
	var (
		assert       = assert.New(t)
		ex           = New("key", "secret", "passphrase")
		expApeSymbol = "APEUSDT"
		since        = time.Now().Add(-24 * time.Hour)
		until        = since.Add(time.Hour)
		options      = &types.TradeQueryOptions{
			StartTime:   &since,
			EndTime:     &until,
			Limit:       queryLimit,
			LastTradeID: 0,
		}
		url      = "/api/v2/spot/trade/fills"
		expOrder = []types.Trade{
			{
				ID:            1149103068190019665,
				OrderID:       1149103067745689603,
				Exchange:      types.ExchangeBitget,
				Price:         fixedpoint.MustNewFromString("1.9959"),
				Quantity:      fixedpoint.MustNewFromString("2.98"),
				QuoteQuantity: fixedpoint.MustNewFromString("5.947782"),
				Symbol:        expApeSymbol,
				Side:          types.SideTypeSell,
				IsBuyer:       false,
				IsMaker:       false,
				Time:          types.Time(types.NewMillisecondTimestampFromInt(1709693441436).Time()),
				Fee:           fixedpoint.MustNewFromString("0.005947782"),
				FeeCurrency:   "USDT",
				FeeDiscounted: false,
			},
			{
				ID:            1149101368775479371,
				OrderID:       1149101366691176462,
				Exchange:      types.ExchangeBitget,
				Price:         fixedpoint.MustNewFromString("2.01"),
				Quantity:      fixedpoint.MustNewFromString("0.0013"),
				QuoteQuantity: fixedpoint.MustNewFromString("0.002613"),
				Symbol:        expApeSymbol,
				Side:          types.SideTypeBuy,
				IsBuyer:       true,
				IsMaker:       true,
				Time:          types.Time(types.NewMillisecondTimestampFromInt(1709693036264).Time()),
				Fee:           fixedpoint.MustNewFromString("0.0000013"),
				FeeCurrency:   "APE",
				FeeDiscounted: false,
			},
			{
				ID:            1149098107964166145,
				OrderID:       1149098107519836161,
				Exchange:      types.ExchangeBitget,
				Price:         fixedpoint.MustNewFromString("2.0087"),
				Quantity:      fixedpoint.MustNewFromString("2.987"),
				QuoteQuantity: fixedpoint.MustNewFromString("5.9999869"),
				Symbol:        expApeSymbol,
				Side:          types.SideTypeBuy,
				IsBuyer:       true,
				IsMaker:       false,
				Time:          types.Time(types.NewMillisecondTimestampFromInt(1709692258826).Time()),
				Fee:           fixedpoint.MustNewFromString("0.002987"),
				FeeCurrency:   "APE",
				FeeDiscounted: false,
			},
			{
				ID:            1149096769322684417,
				OrderID:       1149096768878354435,
				Exchange:      types.ExchangeBitget,
				Price:         fixedpoint.MustNewFromString("2.0068"),
				Quantity:      fixedpoint.MustNewFromString("2.9603"),
				QuoteQuantity: fixedpoint.MustNewFromString("5.94073004"),
				Symbol:        expApeSymbol,
				Side:          types.SideTypeSell,
				IsBuyer:       false,
				IsMaker:       false,
				Time:          types.Time(types.NewMillisecondTimestampFromInt(1709691939669).Time()),
				Fee:           fixedpoint.MustNewFromString("0.00594073004"),
				FeeCurrency:   "USDT",
				FeeDiscounted: false,
			},
		}
	)

	t.Run("succeeds", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_trade_fills_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 4)
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Contains(query, "limit")
			assert.Contains(query, "symbol")
			assert.Equal(query["startTime"], []string{strconv.FormatInt(since.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["endTime"], []string{strconv.FormatInt(until.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			assert.Equal(query["symbol"], []string{expApeSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		orders, err := ex.QueryTrades(context.Background(), expApeSymbol, options)
		assert.NoError(err)
		assert.Equal(expOrder, orders)
	})

	t.Run("succeeds with lastTradeId (not supported) and limit 0", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_trade_fills_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 4)
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Contains(query, "limit")
			assert.Contains(query, "symbol")
			assert.Equal(query["startTime"], []string{strconv.FormatInt(since.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["endTime"], []string{strconv.FormatInt(until.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			assert.Equal(query["symbol"], []string{expApeSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		newOpts := *options
		newOpts.LastTradeID = 50
		newOpts.Limit = 0
		orders, err := ex.QueryTrades(context.Background(), expApeSymbol, &newOpts)
		assert.NoError(err)
		assert.Equal(expOrder, orders)
	})

	t.Run("adjust time range since unexpected since and until", func(t *testing.T) {
		timeNow := time.Now().Truncate(time.Second)
		ex.timeNowFn = func() time.Time {
			return timeNow
		}
		defer func() { ex.timeNowFn = func() time.Time { return time.Now() } }()
		newSince := timeNow.Add(-maxHistoricalDataQueryPeriod * 2)
		expNewSince := timeNow.Add(-maxHistoricalDataQueryPeriod)
		newUntil := timeNow.Add(-maxHistoricalDataQueryPeriod + 24*time.Hour)
		newOpts := *options
		newOpts.StartTime = &newSince
		newOpts.EndTime = &newUntil

		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		// order history
		historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_trade_fills_request.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			query := req.URL.Query()
			assert.Len(query, 4)
			assert.Contains(query, "startTime")
			assert.Contains(query, "endTime")
			assert.Contains(query, "limit")
			assert.Contains(query, "symbol")
			assert.Equal(query["startTime"], []string{strconv.FormatInt(expNewSince.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["endTime"], []string{strconv.FormatInt(newUntil.UnixNano()/int64(time.Millisecond), 10)})
			assert.Equal(query["limit"], []string{strconv.FormatInt(queryLimit, 10)})
			assert.Equal(query["symbol"], []string{expApeSymbol})
			return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
		})

		orders, err := ex.QueryTrades(context.Background(), expApeSymbol, &newOpts)
		assert.NoError(err)
		assert.Equal(expOrder, orders)
	})

	t.Run("failed due to empty since", func(t *testing.T) {
		timeNow := time.Now().Truncate(time.Second)
		ex.timeNowFn = func() time.Time {
			return timeNow
		}
		defer func() { ex.timeNowFn = func() time.Time { return time.Now() } }()
		newSince := timeNow.Add(-maxHistoricalDataQueryPeriod * 2)
		newUntil := timeNow.Add(-maxHistoricalDataQueryPeriod - time.Second)
		newOpts := *options
		newOpts.StartTime = &newSince
		newOpts.EndTime = &newUntil

		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		_, err := ex.QueryTrades(context.Background(), expApeSymbol, &newOpts)
		assert.ErrorContains(err, "before start")
	})

	t.Run("failed due to empty since", func(t *testing.T) {
		newOpts := *options
		newOpts.StartTime = nil

		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		_, err := ex.QueryTrades(context.Background(), expApeSymbol, &newOpts)
		assert.ErrorContains(err, "start time is required")
	})

	t.Run("error", func(t *testing.T) {
		transport := &httptesting.MockTransport{}
		ex.client.HttpClient.Transport = transport

		f, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
		assert.NoError(err)

		transport.GET(url, func(req *http.Request) (*http.Response, error) {
			return httptesting.BuildResponseString(http.StatusBadRequest, string(f)), nil
		})

		_, err = ex.QueryTrades(context.Background(), expApeSymbol, options)
		assert.ErrorContains(err, "Invalid IP")
	})
}
