package bitget

import (
	"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/types"
	"github.com/c9s/bbgo/pkg/types/strint"
)

func Test_toGlobalBalance(t *testing.T) {
	// sample:
	// {
	//        "coinId":"10012",
	//        "coinName":"usdt",
	//        "available":"0",
	//        "frozen":"0",
	//        "lock":"0",
	//        "uTime":"1622697148"
	//    }
	asset := v2.AccountAsset{
		Coin:           "USDT",
		Available:      fixedpoint.NewFromFloat(1.2),
		Frozen:         fixedpoint.NewFromFloat(0.5),
		Locked:         fixedpoint.NewFromFloat(0.5),
		LimitAvailable: fixedpoint.Zero,
		UpdatedTime:    types.NewMillisecondTimestampFromInt(1622697148),
	}

	assert.Equal(t, types.Balance{
		Currency:          "USDT",
		Available:         fixedpoint.NewFromFloat(1.2),
		Locked:            fixedpoint.NewFromFloat(1), // frozen + lock
		Borrowed:          fixedpoint.Zero,
		Interest:          fixedpoint.Zero,
		NetAsset:          fixedpoint.Zero,
		MaxWithdrawAmount: fixedpoint.Zero,
	}, toGlobalBalance(asset))
}

func Test_toGlobalMarket(t *testing.T) {
	// sample:
	// {
	//   "symbol":"BTCUSDT",
	//   "baseCoin":"BTC",
	//   "quoteCoin":"USDT",
	//   "minTradeAmount":"0",
	//   "maxTradeAmount":"10000000000",
	//   "takerFeeRate":"0.002",
	//   "makerFeeRate":"0.002",
	//   "pricePrecision":"2",
	//   "quantityPrecision":"4",
	//   "quotePrecision":"6",
	//   "status":"online",
	//   "minTradeUSDT":"5",
	//   "buyLimitPriceRatio":"0.05",
	//   "sellLimitPriceRatio":"0.05"
	// }
	inst := v2.Symbol{
		Symbol:              "BTCUSDT",
		BaseCoin:            "BTC",
		QuoteCoin:           "USDT",
		MinTradeAmount:      "0",
		MaxTradeAmount:      "10000000000",
		TakerFeeRate:        fixedpoint.NewFromFloat(0.002),
		MakerFeeRate:        fixedpoint.NewFromFloat(0.002),
		PricePrecision:      fixedpoint.NewFromFloat(2),
		QuantityPrecision:   fixedpoint.NewFromFloat(4),
		QuotePrecision:      fixedpoint.NewFromFloat(6),
		MinTradeUSDT:        fixedpoint.NewFromFloat(5),
		Status:              v2.SymbolStatusOnline,
		BuyLimitPriceRatio:  fixedpoint.NewFromFloat(0.05),
		SellLimitPriceRatio: fixedpoint.NewFromFloat(0.05),
	}

	exp := types.Market{
		Exchange:        types.ExchangeBitget,
		Symbol:          inst.Symbol,
		LocalSymbol:     inst.Symbol,
		PricePrecision:  2,
		VolumePrecision: 4,
		QuoteCurrency:   inst.QuoteCoin,
		BaseCurrency:    inst.BaseCoin,
		MinNotional:     inst.MinTradeUSDT,
		MinAmount:       inst.MinTradeUSDT,
		MinQuantity:     fixedpoint.MustNewFromString(inst.MinTradeAmount),
		MaxQuantity:     fixedpoint.MustNewFromString(inst.MaxTradeAmount),
		StepSize:        fixedpoint.NewFromFloat(0.0001),
		MinPrice:        fixedpoint.Zero,
		MaxPrice:        fixedpoint.Zero,
		TickSize:        fixedpoint.NewFromFloat(0.01),
	}

	assert.Equal(t, toGlobalMarket(inst), exp)
}

func Test_toGlobalTicker(t *testing.T) {
	// sample:
	// {
	//   "open":"36465.96",
	//   "symbol":"BTCUSDT",
	//   "high24h":"37040.25",
	//   "low24h":"36202.65",
	//   "lastPr":"36684.42",
	//   "quoteVolume":"311893591.2805",
	//   "baseVolume":"8507.3684",
	//   "usdtVolume":"311893591.280427",
	//   "ts":"1699947106122",
	//   "bidPr":"36684.49",
	//   "askPr":"36684.51",
	//   "bidSz":"0.3812",
	//   "askSz":"0.0133",
	//   "openUtc":"36465.96",
	//   "changeUtc24h":"0.00599",
	//   "change24h":"-0.00426"
	// }
	ticker := v2.Ticker{
		Symbol:       "BTCUSDT",
		High24H:      fixedpoint.NewFromFloat(24175.65),
		Low24H:       fixedpoint.NewFromFloat(23677.75),
		LastPr:       fixedpoint.NewFromFloat(24014.11),
		QuoteVolume:  fixedpoint.NewFromFloat(177689342.3025),
		BaseVolume:   fixedpoint.NewFromFloat(7421.5009),
		UsdtVolume:   fixedpoint.NewFromFloat(177689342.302407),
		Ts:           types.NewMillisecondTimestampFromInt(1660704288118),
		BidPr:        fixedpoint.NewFromFloat(24013.94),
		AskPr:        fixedpoint.NewFromFloat(24014.06),
		BidSz:        fixedpoint.NewFromFloat(0.0663),
		AskSz:        fixedpoint.NewFromFloat(0.0119),
		OpenUtc:      fixedpoint.NewFromFloat(23856.72),
		ChangeUtc24H: fixedpoint.NewFromFloat(0.00301),
		Change24H:    fixedpoint.NewFromFloat(0.00069),
		Open:         fixedpoint.NewFromFloat(23856.72),
	}

	assert.Equal(t, types.Ticker{
		Time:   types.NewMillisecondTimestampFromInt(1660704288118).Time(),
		Volume: fixedpoint.NewFromFloat(7421.5009),
		Last:   fixedpoint.NewFromFloat(24014.11),
		Open:   fixedpoint.NewFromFloat(23856.72),
		High:   fixedpoint.NewFromFloat(24175.65),
		Low:    fixedpoint.NewFromFloat(23677.75),
		Buy:    fixedpoint.NewFromFloat(24013.94),
		Sell:   fixedpoint.NewFromFloat(24014.06),
	}, toGlobalTicker(ticker))
}

func Test_toGlobalSideType(t *testing.T) {
	side, err := toGlobalSideType(v2.SideTypeBuy)
	assert.NoError(t, err)
	assert.Equal(t, types.SideTypeBuy, side)

	side, err = toGlobalSideType(v2.SideTypeSell)
	assert.NoError(t, err)
	assert.Equal(t, types.SideTypeSell, side)

	_, err = toGlobalSideType("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_toGlobalOrderType(t *testing.T) {
	orderType, err := toGlobalOrderType(v2.OrderTypeMarket)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderTypeMarket, orderType)

	orderType, err = toGlobalOrderType(v2.OrderTypeLimit)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderTypeLimit, orderType)

	_, err = toGlobalOrderType("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_toGlobalOrderStatus(t *testing.T) {
	status, err := toGlobalOrderStatus(v2.OrderStatusInit)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusNew, status)

	status, err = toGlobalOrderStatus(v2.OrderStatusNew)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusNew, status)

	status, err = toGlobalOrderStatus(v2.OrderStatusLive)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusNew, status)

	status, err = toGlobalOrderStatus(v2.OrderStatusFilled)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusFilled, status)

	status, err = toGlobalOrderStatus(v2.OrderStatusPartialFilled)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusPartiallyFilled, status)

	status, err = toGlobalOrderStatus(v2.OrderStatusCancelled)
	assert.NoError(t, err)
	assert.Equal(t, types.OrderStatusCanceled, status)

	_, err = toGlobalOrderStatus("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_unfilledOrderToGlobalOrder(t *testing.T) {
	var (
		assert        = assert.New(t)
		orderId       = 1105087175647989764
		unfilledOrder = v2.UnfilledOrder{
			Symbol:           "BTCUSDT",
			OrderId:          strint.Int64(orderId),
			ClientOrderId:    "74b86af3-6098-479c-acac-bfb074c067f3",
			PriceAvg:         fixedpoint.NewFromFloat(1.2),
			Size:             fixedpoint.NewFromFloat(5),
			OrderType:        v2.OrderTypeLimit,
			Side:             v2.SideTypeBuy,
			Status:           v2.OrderStatusLive,
			BasePrice:        fixedpoint.NewFromFloat(0),
			BaseVolume:       fixedpoint.NewFromFloat(0),
			QuoteVolume:      fixedpoint.NewFromFloat(0),
			EnterPointSource: "API",
			OrderSource:      "normal",
			CreatedTime:      types.NewMillisecondTimestampFromInt(1660704288118),
			UpdatedTime:      types.NewMillisecondTimestampFromInt(1660704288118),
		}
	)

	t.Run("succeeds with limit order", func(t *testing.T) {
		order, err := unfilledOrderToGlobalOrder(unfilledOrder)
		assert.NoError(err)
		assert.Equal(&types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
				Symbol:        "BTCUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeLimit,
				Quantity:      fixedpoint.NewFromFloat(5),
				Price:         fixedpoint.NewFromFloat(1.2),
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(orderId),
			Status:           types.OrderStatusNew,
			ExecutedQuantity: fixedpoint.NewFromFloat(0),
			IsWorking:        true,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
		}, order)
	})

	t.Run("succeeds with market buy order", func(t *testing.T) {
		unfilledOrder2 := unfilledOrder
		unfilledOrder2.OrderType = v2.OrderTypeMarket
		unfilledOrder2.Side = v2.SideTypeBuy
		unfilledOrder2.Size = unfilledOrder2.PriceAvg.Mul(unfilledOrder2.Size)
		unfilledOrder2.PriceAvg = fixedpoint.Zero
		unfilledOrder2.Status = v2.OrderStatusNew

		order, err := unfilledOrderToGlobalOrder(unfilledOrder2)
		assert.NoError(err)
		assert.Equal(&types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
				Symbol:        "BTCUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeMarket,
				Quantity:      fixedpoint.Zero,
				Price:         fixedpoint.Zero,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(orderId),
			Status:           types.OrderStatusNew,
			ExecutedQuantity: fixedpoint.Zero,
			IsWorking:        true,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
		}, order)
	})

	t.Run("succeeds with market sell order", func(t *testing.T) {
		unfilledOrder2 := unfilledOrder
		unfilledOrder2.OrderType = v2.OrderTypeMarket
		unfilledOrder2.Side = v2.SideTypeSell
		unfilledOrder2.PriceAvg = fixedpoint.Zero
		unfilledOrder2.Status = v2.OrderStatusNew

		order, err := unfilledOrderToGlobalOrder(unfilledOrder2)
		assert.NoError(err)
		assert.Equal(&types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
				Symbol:        "BTCUSDT",
				Side:          types.SideTypeSell,
				Type:          types.OrderTypeMarket,
				Quantity:      fixedpoint.NewFromInt(5),
				Price:         fixedpoint.Zero,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(orderId),
			Status:           types.OrderStatusNew,
			ExecutedQuantity: fixedpoint.Zero,
			IsWorking:        true,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
		}, order)
	})

	t.Run("failed to convert side", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.Side = "xxx"

		_, err := unfilledOrderToGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})

	t.Run("failed to convert oder type", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.OrderType = "xxx"

		_, err := unfilledOrderToGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})

	t.Run("failed to convert oder status", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.Status = "xxx"

		_, err := unfilledOrderToGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})
}

func Test_toGlobalOrder(t *testing.T) {
	var (
		assert        = assert.New(t)
		orderId       = 1105087175647989764
		unfilledOrder = v2.OrderDetail{
			UserId:           123456,
			Symbol:           "BTCUSDT",
			OrderId:          strint.Int64(orderId),
			ClientOrderId:    "74b86af3-6098-479c-acac-bfb074c067f3",
			Price:            fixedpoint.NewFromFloat(1.2),
			Size:             fixedpoint.NewFromFloat(5),
			OrderType:        v2.OrderTypeLimit,
			Side:             v2.SideTypeBuy,
			Status:           v2.OrderStatusFilled,
			PriceAvg:         fixedpoint.NewFromFloat(1.4),
			BaseVolume:       fixedpoint.NewFromFloat(5),
			QuoteVolume:      fixedpoint.NewFromFloat(7.0005),
			EnterPointSource: "API",
			FeeDetailRaw:     `{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}`,
			OrderSource:      "normal",
			CreatedTime:      types.NewMillisecondTimestampFromInt(1660704288118),
			UpdatedTime:      types.NewMillisecondTimestampFromInt(1660704288118),
		}

		expOrder = &types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
				Symbol:        "BTCUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeLimit,
				Quantity:      fixedpoint.NewFromFloat(5),
				Price:         fixedpoint.NewFromFloat(1.2),
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(orderId),
			Status:           types.OrderStatusFilled,
			ExecutedQuantity: fixedpoint.NewFromFloat(5),
			IsWorking:        false,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
		}
	)

	t.Run("succeeds with limit buy", func(t *testing.T) {
		order, err := toGlobalOrder(unfilledOrder)
		assert.NoError(err)
		assert.Equal(expOrder, order)
	})

	t.Run("succeeds with limit sell", func(t *testing.T) {
		newUnfilledOrder := unfilledOrder
		newUnfilledOrder.Side = v2.SideTypeSell

		newExpOrder := *expOrder
		newExpOrder.Side = types.SideTypeSell

		order, err := toGlobalOrder(newUnfilledOrder)
		assert.NoError(err)
		assert.Equal(&newExpOrder, order)
	})

	t.Run("succeeds with market sell", func(t *testing.T) {
		newUnfilledOrder := unfilledOrder
		newUnfilledOrder.Side = v2.SideTypeSell
		newUnfilledOrder.OrderType = v2.OrderTypeMarket

		newExpOrder := *expOrder
		newExpOrder.Side = types.SideTypeSell
		newExpOrder.Type = types.OrderTypeMarket
		newExpOrder.Price = newUnfilledOrder.PriceAvg

		order, err := toGlobalOrder(newUnfilledOrder)
		assert.NoError(err)
		assert.Equal(&newExpOrder, order)
	})

	t.Run("succeeds with market buy", func(t *testing.T) {
		newUnfilledOrder := unfilledOrder
		newUnfilledOrder.Side = v2.SideTypeBuy
		newUnfilledOrder.OrderType = v2.OrderTypeMarket

		newExpOrder := *expOrder
		newExpOrder.Side = types.SideTypeBuy
		newExpOrder.Type = types.OrderTypeMarket
		newExpOrder.Price = newUnfilledOrder.PriceAvg
		newExpOrder.Quantity = newUnfilledOrder.BaseVolume

		order, err := toGlobalOrder(newUnfilledOrder)
		assert.NoError(err)
		assert.Equal(&newExpOrder, order)
	})

	t.Run("succeeds with limit buy", func(t *testing.T) {
		order, err := toGlobalOrder(unfilledOrder)
		assert.NoError(err)
		assert.Equal(&types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
				Symbol:        "BTCUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeLimit,
				Quantity:      fixedpoint.NewFromFloat(5),
				Price:         fixedpoint.NewFromFloat(1.2),
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(orderId),
			Status:           types.OrderStatusFilled,
			ExecutedQuantity: fixedpoint.NewFromFloat(5),
			IsWorking:        false,
			CreationTime:     types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
			UpdateTime:       types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
		}, order)
	})

	t.Run("failed to convert side", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.Side = "xxx"

		_, err := toGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})

	t.Run("failed to convert oder type", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.OrderType = "xxx"

		_, err := toGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})

	t.Run("failed to convert oder status", func(t *testing.T) {
		newOrder := unfilledOrder
		newOrder.Status = "xxx"

		_, err := toGlobalOrder(newOrder)
		assert.ErrorContains(err, "xxx")
	})
}

func Test_processMarketBuyQuantity(t *testing.T) {
	var (
		assert            = assert.New(t)
		filledBaseCoinQty = fixedpoint.NewFromFloat(3.5648)
		filledPrice       = fixedpoint.NewFromFloat(4.99998848)
		priceAvg          = fixedpoint.NewFromFloat(1.4026)
		buyQty            = fixedpoint.NewFromFloat(5)
	)

	t.Run("zero quantity on Init/New/Live/Cancelled", func(t *testing.T) {
		qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusInit)
		assert.NoError(err)
		assert.Equal(fixedpoint.Zero, qty)

		qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusNew)
		assert.NoError(err)
		assert.Equal(fixedpoint.Zero, qty)

		qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusLive)
		assert.NoError(err)
		assert.Equal(fixedpoint.Zero, qty)

		qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusCancelled)
		assert.NoError(err)
		assert.Equal(fixedpoint.Zero, qty)
	})

	t.Run("5 on PartialFilled", func(t *testing.T) {
		priceAvg := fixedpoint.NewFromFloat(2)
		buyQty := fixedpoint.NewFromFloat(10)
		filledPrice := fixedpoint.NewFromFloat(4)
		filledBaseCoinQty := fixedpoint.NewFromFloat(2)

		qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusPartialFilled)
		assert.NoError(err)
		assert.Equal(fixedpoint.NewFromFloat(5), qty)
	})

	t.Run("3.5648 on Filled", func(t *testing.T) {
		qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusFilled)
		assert.NoError(err)
		assert.Equal(fixedpoint.NewFromFloat(3.5648), qty)
	})

	t.Run("unexpected order status", func(t *testing.T) {
		_, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, "xxx")
		assert.ErrorContains(err, "xxx")
	})
}

func Test_toLocalOrderType(t *testing.T) {
	orderType, err := toLocalOrderType(types.OrderTypeLimit)
	assert.NoError(t, err)
	assert.Equal(t, v2.OrderTypeLimit, orderType)

	orderType, err = toLocalOrderType(types.OrderTypeMarket)
	assert.NoError(t, err)
	assert.Equal(t, v2.OrderTypeMarket, orderType)

	_, err = toLocalOrderType("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_toLocalSide(t *testing.T) {
	orderType, err := toLocalSide(types.SideTypeSell)
	assert.NoError(t, err)
	assert.Equal(t, v2.SideTypeSell, orderType)

	orderType, err = toLocalSide(types.SideTypeBuy)
	assert.NoError(t, err)
	assert.Equal(t, v2.SideTypeBuy, orderType)

	_, err = toLocalOrderType("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_isMaker(t *testing.T) {
	isM, err := isMaker(v2.TradeTaker)
	assert.NoError(t, err)
	assert.False(t, isM)

	isM, err = isMaker(v2.TradeMaker)
	assert.NoError(t, err)
	assert.True(t, isM)

	_, err = isMaker("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_isFeeDiscount(t *testing.T) {
	isDiscount, err := isFeeDiscount(v2.DiscountNo)
	assert.NoError(t, err)
	assert.False(t, isDiscount)

	isDiscount, err = isFeeDiscount(v2.DiscountYes)
	assert.NoError(t, err)
	assert.True(t, isDiscount)

	_, err = isFeeDiscount("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func Test_toGlobalTrade(t *testing.T) {
	// {
	//   "userId":"8672173294",
	//   "symbol":"APEUSDT",
	//   "orderId":"1104337778433757184",
	//   "tradeId":"1104337778504044545",
	//   "orderType":"limit",
	//   "side":"sell",
	//   "priceAvg":"1.4001",
	//   "newSize":"5",
	//   "amount":"7.0005",
	//   "feeDetail":{
	//      "deduction":"no",
	//      "feeCoin":"USDT",
	//      "totalDeductionFee":"",
	//      "totalFee":"-0.0070005"
	//   },
	//   "tradeScope":"taker",
	//   "cTime":"1699020564676",
	//   "uTime":"1699020564687"
	// }
	trade := v2.Trade{
		UserId:    strint.Int64(8672173294),
		Symbol:    "APEUSDT",
		OrderId:   strint.Int64(1104337778433757184),
		TradeId:   strint.Int64(1104337778504044545),
		OrderType: v2.OrderTypeLimit,
		Side:      v2.SideTypeSell,
		PriceAvg:  fixedpoint.NewFromFloat(1.4001),
		Size:      fixedpoint.NewFromFloat(5),
		Amount:    fixedpoint.NewFromFloat(7.0005),
		FeeDetail: v2.TradeFee{
			Deduction:         "no",
			FeeCoin:           "USDT",
			TotalDeductionFee: fixedpoint.Zero,
			TotalFee:          fixedpoint.NewFromFloat(-0.0070005),
		},
		TradeScope:  v2.TradeTaker,
		CreatedTime: types.NewMillisecondTimestampFromInt(1699020564676),
		UpdatedTime: types.NewMillisecondTimestampFromInt(1699020564687),
	}

	res, err := toGlobalTrade(trade)
	assert.NoError(t, err)
	assert.Equal(t, &types.Trade{
		ID:            uint64(1104337778504044545),
		OrderID:       uint64(1104337778433757184),
		Exchange:      types.ExchangeBitget,
		Price:         fixedpoint.NewFromFloat(1.4001),
		Quantity:      fixedpoint.NewFromFloat(5),
		QuoteQuantity: fixedpoint.NewFromFloat(7.0005),
		Symbol:        "APEUSDT",
		Side:          types.SideTypeSell,
		IsBuyer:       false,
		IsMaker:       false,
		Time:          types.Time(types.NewMillisecondTimestampFromInt(1699020564676)),
		Fee:           fixedpoint.NewFromFloat(0.0070005),
		FeeCurrency:   "USDT",
		FeeDiscounted: false,
	}, res)
}

func Test_toGlobalBalanceMap(t *testing.T) {
	assert.Equal(t, types.BalanceMap{
		"BTC": {
			Currency:  "BTC",
			Available: fixedpoint.NewFromFloat(0.5),
			Locked:    fixedpoint.NewFromFloat(0.6 + 0.7),
		},
	}, toGlobalBalanceMap([]Balance{
		{
			Coin:           "BTC",
			Available:      fixedpoint.NewFromFloat(0.5),
			Frozen:         fixedpoint.NewFromFloat(0.6),
			Locked:         fixedpoint.NewFromFloat(0.7),
			LimitAvailable: fixedpoint.Zero,
			UpdatedTime:    types.NewMillisecondTimestampFromInt(1699020564676),
		},
	}))
}

func Test_toGlobalKLines(t *testing.T) {
	symbol := "BTCUSDT"
	interval := types.Interval15m

	resp := v2.KLineResponse{
		/*
			[
				{
					"Ts": "1699816800000",
					"OpenPrice": 29045.3,
					"HighPrice": 29228.56,
					"LowPrice": 29045.3,
					"ClosePrice": 29228.56,
					"Volume": 9.265593,
					"QuoteVolume": 270447.43520753,
					"UsdtVolume": 270447.43520753
				},
				{
					"Ts": "1699816800000",
					"OpenPrice": 29167.33,
					"HighPrice": 29229.08,
					"LowPrice": 29000,
					"ClosePrice": 29045.3,
					"Volume": 9.295508,
					"QuoteVolume": 270816.87513775,
					"UsdtVolume": 270816.87513775
				}
			]
		*/
		{
			Ts:          types.NewMillisecondTimestampFromInt(1691486100000),
			Open:        fixedpoint.NewFromFloat(29045.3),
			High:        fixedpoint.NewFromFloat(29228.56),
			Low:         fixedpoint.NewFromFloat(29045.3),
			Close:       fixedpoint.NewFromFloat(29228.56),
			Volume:      fixedpoint.NewFromFloat(9.265593),
			QuoteVolume: fixedpoint.NewFromFloat(270447.43520753),
			UsdtVolume:  fixedpoint.NewFromFloat(270447.43520753),
		},
		{
			Ts:          types.NewMillisecondTimestampFromInt(1691487000000),
			Open:        fixedpoint.NewFromFloat(29167.33),
			High:        fixedpoint.NewFromFloat(29229.08),
			Low:         fixedpoint.NewFromFloat(29000),
			Close:       fixedpoint.NewFromFloat(29045.3),
			Volume:      fixedpoint.NewFromFloat(9.295508),
			QuoteVolume: fixedpoint.NewFromFloat(270816.87513775),
			UsdtVolume:  fixedpoint.NewFromFloat(270447.43520753),
		},
	}

	expKlines := []types.KLine{
		{
			Exchange:    types.ExchangeBitget,
			Symbol:      symbol,
			StartTime:   types.Time(resp[0].Ts.Time()),
			EndTime:     types.Time(resp[0].Ts.Time().Add(interval.Duration() - time.Millisecond)),
			Interval:    interval,
			Open:        fixedpoint.NewFromFloat(29045.3),
			Close:       fixedpoint.NewFromFloat(29228.56),
			High:        fixedpoint.NewFromFloat(29228.56),
			Low:         fixedpoint.NewFromFloat(29045.3),
			Volume:      fixedpoint.NewFromFloat(9.265593),
			QuoteVolume: fixedpoint.NewFromFloat(270447.43520753),
			Closed:      false,
		},
		{
			Exchange:    types.ExchangeBitget,
			Symbol:      symbol,
			StartTime:   types.Time(resp[1].Ts.Time()),
			EndTime:     types.Time(resp[1].Ts.Time().Add(interval.Duration() - time.Millisecond)),
			Interval:    interval,
			Open:        fixedpoint.NewFromFloat(29167.33),
			Close:       fixedpoint.NewFromFloat(29045.3),
			High:        fixedpoint.NewFromFloat(29229.08),
			Low:         fixedpoint.NewFromFloat(29000),
			Volume:      fixedpoint.NewFromFloat(9.295508),
			QuoteVolume: fixedpoint.NewFromFloat(270816.87513775),
			Closed:      false,
		},
	}

	assert.Equal(t, toGlobalKLines(symbol, interval, resp), expKlines)
}

func Test_toGlobalTimeInForce(t *testing.T) {
	force, err := toGlobalTimeInForce(v2.OrderForceFOK)
	assert.NoError(t, err)
	assert.Equal(t, types.TimeInForceFOK, force)

	force, err = toGlobalTimeInForce(v2.OrderForceGTC)
	assert.NoError(t, err)
	assert.Equal(t, types.TimeInForceGTC, force)

	force, err = toGlobalTimeInForce(v2.OrderForcePostOnly)
	assert.NoError(t, err)
	assert.Equal(t, types.TimeInForceGTC, force)

	force, err = toGlobalTimeInForce(v2.OrderForceIOC)
	assert.NoError(t, err)
	assert.Equal(t, types.TimeInForceIOC, force)

	_, err = toGlobalTimeInForce("xxx")
	assert.ErrorContains(t, err, "xxx")
}

func TestOrder_processMarketBuyQuantity(t *testing.T) {
	t.Run("zero qty", func(t *testing.T) {
		o := Order{}
		for _, s := range []v2.OrderStatus{v2.OrderStatusLive, v2.OrderStatusNew, v2.OrderStatusInit, v2.OrderStatusCancelled} {
			o.Status = s
			qty, err := o.processMarketBuyQuantity()
			assert.NoError(t, err)
			assert.Equal(t, fixedpoint.Zero, qty)
		}
	})

	t.Run("calculate qty", func(t *testing.T) {
		o := Order{
			NewSize: fixedpoint.NewFromFloat(2),
			Trade: Trade{
				FillPrice: fixedpoint.NewFromFloat(1),
			},
			Status: v2.OrderStatusPartialFilled,
		}
		qty, err := o.processMarketBuyQuantity()
		assert.NoError(t, err)
		assert.Equal(t, fixedpoint.NewFromFloat(2), qty)
	})

	t.Run("return accumulated balance", func(t *testing.T) {
		o := Order{
			AccBaseVolume: fixedpoint.NewFromFloat(5),
			Status:        v2.OrderStatusFilled,
		}
		qty, err := o.processMarketBuyQuantity()
		assert.NoError(t, err)
		assert.Equal(t, fixedpoint.NewFromFloat(5), qty)
	})

	t.Run("unexpected status", func(t *testing.T) {
		o := Order{
			Status: "xxx",
		}
		_, err := o.processMarketBuyQuantity()
		assert.ErrorContains(t, err, "xxx")
	})
}

func TestOrder_toGlobalOrder(t *testing.T) {
	o := Order{
		Trade: Trade{
			FillPrice:   fixedpoint.NewFromFloat(0.49016),
			TradeId:     strint.Int64(1107950490073112582),
			BaseVolume:  fixedpoint.NewFromFloat(33.6558),
			FillTime:    types.NewMillisecondTimestampFromInt(1699881902235),
			FillFee:     fixedpoint.NewFromFloat(-0.0336558),
			FillFeeCoin: "BGB",
			TradeScope:  "T",
		},
		InstId:           "BGBUSDT",
		OrderId:          strint.Int64(1107950489998626816),
		ClientOrderId:    "cc73aab9-1e44-4022-8458-60d8c6a08753",
		NewSize:          fixedpoint.NewFromFloat(39.0),
		Notional:         fixedpoint.NewFromFloat(39.0),
		OrderType:        v2.OrderTypeMarket,
		Force:            v2.OrderForceGTC,
		Side:             v2.SideTypeBuy,
		AccBaseVolume:    fixedpoint.NewFromFloat(33.6558),
		PriceAvg:         fixedpoint.NewFromFloat(0.49016),
		Status:           v2.OrderStatusPartialFilled,
		CreatedTime:      types.NewMillisecondTimestampFromInt(1699881902217),
		UpdatedTime:      types.NewMillisecondTimestampFromInt(1699881902248),
		FeeDetail:        nil,
		EnterPointSource: "API",
	}

	// market buy example:
	//      {
	//         "instId":"BGBUSDT",
	//         "orderId":"1107950489998626816",
	//         "clientOid":"cc73aab9-1e44-4022-8458-60d8c6a08753",
	//         "newSize":"39.0000",
	//         "notional":"39.000000",
	//         "orderType":"market",
	//         "force":"gtc",
	//         "side":"buy",
	//         "fillPrice":"0.49016",
	//         "tradeId":"1107950490073112582",
	//         "baseVolume":"33.6558",
	//         "fillTime":"1699881902235",
	//         "fillFee":"-0.0336558",
	//         "fillFeeCoin":"BGB",
	//         "tradeScope":"T",
	//         "accBaseVolume":"33.6558",
	//         "priceAvg":"0.49016",
	//         "status":"partially_filled",
	//         "cTime":"1699881902217",
	//         "uTime":"1699881902248",
	//         "feeDetail":[
	//            {
	//               "feeCoin":"BGB",
	//               "fee":"-0.0336558"
	//            }
	//         ],
	//         "enterPointSource":"API"
	//      }
	t.Run("market buy", func(t *testing.T) {
		newO := o
		res, err := newO.toGlobalOrder()
		assert.NoError(t, err)
		assert.Equal(t, types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753",
				Symbol:        "BGBUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeMarket,
				Quantity:      newO.NewSize.Div(newO.FillPrice),
				Price:         newO.PriceAvg,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(newO.OrderId),
			Status:           types.OrderStatusPartiallyFilled,
			ExecutedQuantity: newO.AccBaseVolume,
			IsWorking:        newO.Status.IsWorking(),
			CreationTime:     types.Time(newO.CreatedTime),
			UpdateTime:       types.Time(newO.UpdatedTime),
		}, res)
	})

	// market sell example:
	//      {
	//         "instId":"BGBUSDT",
	//         "orderId":"1107940456212631553",
	//         "clientOid":"088bb971-858e-48e2-b503-85c3274edd89",
	//         "newSize":"285.0000",
	//         "orderType":"market",
	//         "force":"gtc",
	//         "side":"sell",
	//         "fillPrice":"0.48706",
	//         "tradeId":"1107940456278728706",
	//         "baseVolume":"22.5840",
	//         "fillTime":"1699879509992",
	//         "fillFee":"-0.01099976304",
	//         "fillFeeCoin":"USDT",
	//         "tradeScope":"T",
	//         "accBaseVolume":"45.1675",
	//         "priceAvg":"0.48706",
	//         "status":"partially_filled",
	//         "cTime":"1699879509976",
	//         "uTime":"1699879510007",
	//         "feeDetail":[
	//            {
	//               "feeCoin":"USDT",
	//               "fee":"-0.02199928255"
	//            }
	//         ],
	//         "enterPointSource":"API"
	//      }
	t.Run("market sell", func(t *testing.T) {
		newO := o
		newO.OrderType = v2.OrderTypeMarket
		newO.Side = v2.SideTypeSell

		res, err := newO.toGlobalOrder()
		assert.NoError(t, err)
		assert.Equal(t, types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753",
				Symbol:        "BGBUSDT",
				Side:          types.SideTypeSell,
				Type:          types.OrderTypeMarket,
				Quantity:      newO.NewSize,
				Price:         newO.PriceAvg,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(newO.OrderId),
			Status:           types.OrderStatusPartiallyFilled,
			ExecutedQuantity: newO.AccBaseVolume,
			IsWorking:        newO.Status.IsWorking(),
			CreationTime:     types.Time(newO.CreatedTime),
			UpdateTime:       types.Time(newO.UpdatedTime),
		}, res)
	})

	// limit buy example:
	//      {
	//         "instId":"BGBUSDT",
	//         "orderId":"1107955329902481408",
	//         "clientOid":"c578164a-bf34-44ba-8bb7-a1538f33b1b8",
	//         "price":"0.49998",
	//         "newSize":"24.9990",
	//         "notional":"24.999000",
	//         "orderType":"limit",
	//         "force":"gtc",
	//         "side":"buy",
	//         "fillPrice":"0.49998",
	//         "tradeId":"1107955401758285828",
	//         "baseVolume":"15.9404",
	//         "fillTime":"1699883073272",
	//         "fillFee":"-0.0159404",
	//         "fillFeeCoin":"BGB",
	//         "tradeScope":"M",
	//         "accBaseVolume":"15.9404",
	//         "priceAvg":"0.49998",
	//         "status":"partially_filled",
	//         "cTime":"1699883056140",
	//         "uTime":"1699883073285",
	//         "feeDetail":[
	//            {
	//               "feeCoin":"BGB",
	//               "fee":"-0.0159404"
	//            }
	//         ],
	//         "enterPointSource":"API"
	//      }
	t.Run("limit buy", func(t *testing.T) {
		newO := o
		newO.Price = fixedpoint.NewFromFloat(0.49998)
		newO.OrderType = v2.OrderTypeLimit

		res, err := newO.toGlobalOrder()
		assert.NoError(t, err)
		assert.Equal(t, types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753",
				Symbol:        "BGBUSDT",
				Side:          types.SideTypeBuy,
				Type:          types.OrderTypeLimit,
				Quantity:      newO.NewSize,
				Price:         newO.Price,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(newO.OrderId),
			Status:           types.OrderStatusPartiallyFilled,
			ExecutedQuantity: newO.AccBaseVolume,
			IsWorking:        newO.Status.IsWorking(),
			CreationTime:     types.Time(newO.CreatedTime),
			UpdateTime:       types.Time(newO.UpdatedTime),
		}, res)
	})

	// limit sell example:
	//      {
	//         "instId":"BGBUSDT",
	//         "orderId":"1107936497259417600",
	//         "clientOid":"02d4592e-091c-4b5a-aef3-6a7cf57b5e82",
	//         "price":"0.48710",
	//         "newSize":"280.0000",
	//         "orderType":"limit",
	//         "force":"gtc",
	//         "side":"sell",
	//         "fillPrice":"0.48710",
	//         "tradeId":"1107937053540556809",
	//         "baseVolume":"41.0593",
	//         "fillTime":"1699878698716",
	//         "fillFee":"-0.01999998503",
	//         "fillFeeCoin":"USDT",
	//         "tradeScope":"M",
	//         "accBaseVolume":"146.3209",
	//         "priceAvg":"0.48710",
	//         "status":"partially_filled",
	//         "cTime":"1699878566088",
	//         "uTime":"1699878698746",
	//         "feeDetail":[
	//            {
	//               "feeCoin":"USDT",
	//               "fee":"-0.07127291039"
	//            }
	//         ],
	//         "enterPointSource":"API"
	//      }
	t.Run("limit sell", func(t *testing.T) {
		newO := o
		newO.OrderType = v2.OrderTypeLimit
		newO.Side = v2.SideTypeSell
		newO.Price = fixedpoint.NewFromFloat(0.48710)

		res, err := newO.toGlobalOrder()
		assert.NoError(t, err)
		assert.Equal(t, types.Order{
			SubmitOrder: types.SubmitOrder{
				ClientOrderID: "cc73aab9-1e44-4022-8458-60d8c6a08753",
				Symbol:        "BGBUSDT",
				Side:          types.SideTypeSell,
				Type:          types.OrderTypeLimit,
				Quantity:      newO.NewSize,
				Price:         newO.Price,
				TimeInForce:   types.TimeInForceGTC,
			},
			Exchange:         types.ExchangeBitget,
			OrderID:          uint64(newO.OrderId),
			Status:           types.OrderStatusPartiallyFilled,
			ExecutedQuantity: newO.AccBaseVolume,
			IsWorking:        newO.Status.IsWorking(),
			CreationTime:     types.Time(newO.CreatedTime),
			UpdateTime:       types.Time(newO.UpdatedTime),
		}, res)
	})

	t.Run("unexpected status", func(t *testing.T) {
		newO := o
		newO.Status = "xxx"
		_, err := newO.toGlobalOrder()
		assert.ErrorContains(t, err, "xxx")
	})

	t.Run("unexpected time-in-force", func(t *testing.T) {
		newO := o
		newO.Force = "xxx"
		_, err := newO.toGlobalOrder()
		assert.ErrorContains(t, err, "xxx")
	})

	t.Run("unexpected order type", func(t *testing.T) {
		newO := o
		newO.OrderType = "xxx"
		_, err := newO.toGlobalOrder()
		assert.ErrorContains(t, err, "xxx")
	})

	t.Run("unexpected side", func(t *testing.T) {
		newO := o
		newO.Side = "xxx"
		_, err := newO.toGlobalOrder()
		assert.ErrorContains(t, err, "xxx")
	})
}

func TestOrder_toGlobalTrade(t *testing.T) {
	// market buy example:
	//      {
	//         "instId":"BGBUSDT",
	//         "orderId":"1107950489998626816",
	//         "clientOid":"cc73aab9-1e44-4022-8458-60d8c6a08753",
	//         "newSize":"39.0000",
	//         "notional":"39.000000",
	//         "orderType":"market",
	//         "force":"gtc",
	//         "side":"buy",
	//         "fillPrice":"0.49016",
	//         "tradeId":"1107950490073112582",
	//         "baseVolume":"33.6558",
	//         "fillTime":"1699881902235",
	//         "fillFee":"-0.0336558",
	//         "fillFeeCoin":"BGB",
	//         "tradeScope":"T",
	//         "accBaseVolume":"33.6558",
	//         "priceAvg":"0.49016",
	//         "status":"partially_filled",
	//         "cTime":"1699881902217",
	//         "uTime":"1699881902248",
	//         "feeDetail":[
	//            {
	//               "feeCoin":"BGB",
	//               "fee":"-0.0336558"
	//            }
	//         ],
	//         "enterPointSource":"API"
	//      }
	o := Order{
		Trade: Trade{
			FillPrice:   fixedpoint.NewFromFloat(0.49016),
			TradeId:     strint.Int64(1107950490073112582),
			BaseVolume:  fixedpoint.NewFromFloat(33.6558),
			FillTime:    types.NewMillisecondTimestampFromInt(1699881902235),
			FillFee:     fixedpoint.NewFromFloat(-0.0336558),
			FillFeeCoin: "BGB",
			TradeScope:  "T",
		},
		InstId:           "BGBUSDT",
		OrderId:          strint.Int64(1107950489998626816),
		ClientOrderId:    "cc73aab9-1e44-4022-8458-60d8c6a08753",
		NewSize:          fixedpoint.NewFromFloat(39.0),
		Notional:         fixedpoint.NewFromFloat(39.0),
		OrderType:        v2.OrderTypeMarket,
		Force:            v2.OrderForceGTC,
		Side:             v2.SideTypeBuy,
		AccBaseVolume:    fixedpoint.NewFromFloat(33.6558),
		PriceAvg:         fixedpoint.NewFromFloat(0.49016),
		Status:           v2.OrderStatusPartialFilled,
		CreatedTime:      types.NewMillisecondTimestampFromInt(1699881902217),
		UpdatedTime:      types.NewMillisecondTimestampFromInt(1699881902248),
		FeeDetail:        nil,
		EnterPointSource: "API",
	}

	t.Run("succeeds", func(t *testing.T) {
		res, err := o.toGlobalTrade()
		assert.NoError(t, err)
		assert.Equal(t, types.Trade{
			ID:            uint64(o.TradeId),
			OrderID:       uint64(o.OrderId),
			Exchange:      types.ExchangeBitget,
			Price:         o.FillPrice,
			Quantity:      o.BaseVolume,
			QuoteQuantity: o.FillPrice.Mul(o.BaseVolume),
			Symbol:        "BGBUSDT",
			Side:          types.SideTypeBuy,
			IsBuyer:       true,
			IsMaker:       false,
			Time:          types.Time(o.FillTime),
			Fee:           o.FillFee.Abs(),
			FeeCurrency:   "BGB",
		}, res)
	})

	t.Run("unexpected trade scope", func(t *testing.T) {
		newO := o
		newO.TradeScope = "xxx"
		_, err := newO.toGlobalTrade()
		assert.ErrorContains(t, err, "xxx")
	})

	t.Run("unexpected side type", func(t *testing.T) {
		newO := o
		newO.Side = "xxx"
		_, err := newO.toGlobalTrade()
		assert.ErrorContains(t, err, "xxx")
	})

	t.Run("unexpected side type", func(t *testing.T) {
		newO := o
		newO.Status = "xxx"
		_, err := newO.toGlobalTrade()
		assert.ErrorContains(t, err, "xxx")
	})
}
