package kline

import (
	"math/rand"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/database"
	"github.com/thrasher-corp/gocryptotrader/database/drivers"
	"github.com/thrasher-corp/gocryptotrader/database/repository/candle"
	"github.com/thrasher-corp/gocryptotrader/database/repository/exchange"
	"github.com/thrasher-corp/gocryptotrader/database/testhelpers"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)

var (
	verbose       = false
	testExchanges = []exchange.Details{{Name: "one"}}
)

func TestValidateData(t *testing.T) {
	t.Parallel()
	err := validateData(nil)
	if err == nil {
		t.Error("error cannot be nil")
	}

	var empty []order.TradeHistory
	err = validateData(empty)
	if err == nil {
		t.Error("error cannot be nil")
	}

	tn := time.Now()
	trade1 := []order.TradeHistory{
		{Timestamp: tn.Add(2 * time.Minute), TID: "2"},
		{Timestamp: tn.Add(time.Minute), TID: "1"},
		{Timestamp: tn.Add(3 * time.Minute), TID: "3"},
	}

	err = validateData(trade1)
	if err == nil {
		t.Error("error cannot be nil")
	}

	trade2 := []order.TradeHistory{
		{Timestamp: tn.Add(2 * time.Minute), TID: "2", Amount: 1, Price: 0},
	}

	err = validateData(trade2)
	if err == nil {
		t.Error("error cannot be nil")
	}

	trade3 := []order.TradeHistory{
		{TID: "2", Amount: 1, Price: 0},
	}

	err = validateData(trade3)
	if err == nil {
		t.Error("error cannot be nil")
	}

	trade4 := []order.TradeHistory{
		{Timestamp: tn.Add(2 * time.Minute), TID: "2", Amount: 1, Price: 1000},
		{Timestamp: tn.Add(time.Minute), TID: "1", Amount: 1, Price: 1001},
		{Timestamp: tn.Add(3 * time.Minute), TID: "3", Amount: 1, Price: 1001.5},
	}

	err = validateData(trade4)
	assert.NoError(t, err)

	if trade4[0].TID != "1" || trade4[1].TID != "2" || trade4[2].TID != "3" {
		t.Error("trade history sorted incorrectly")
	}
}

func TestCreateKline(t *testing.T) {
	t.Parallel()

	pair := currency.NewBTCUSD()
	_, err := CreateKline(nil, OneMin, pair, asset.Spot, "Binance")
	require.ErrorIs(t, err, errInsufficientTradeData)

	tradeTotal := 24000
	trades := make([]order.TradeHistory, tradeTotal)
	execution := time.Now()
	for x := range tradeTotal {
		price, rndTime := 1000+float64(rand.Intn(1000)), rand.Intn(10) //nolint:gosec // no need to import crypo/rand for testing
		execution = execution.Add(time.Duration(rndTime) * time.Second)
		trades[x] = order.TradeHistory{
			Timestamp: execution,
			Amount:    1, // Keep as one for counting
			Price:     price,
		}
	}

	_, err = CreateKline(trades, 0, pair, asset.Spot, "Binance")
	require.ErrorIs(t, err, ErrInvalidInterval)

	c, err := CreateKline(trades, OneMin, pair, asset.Spot, "Binance")
	if err != nil {
		t.Fatal(err)
	}

	var amounts float64
	for x := range c.Candles {
		amounts += c.Candles[x].Volume
	}
	if amounts != float64(tradeTotal) {
		t.Fatalf("received: '%v' but expected '%v'", amounts, float64(tradeTotal))
	}
}

func TestKlineWord(t *testing.T) {
	t.Parallel()
	if OneDay.Word() != "oneday" {
		t.Fatalf("unexpected result: %v", OneDay.Word())
	}
}

func TestKlineDuration(t *testing.T) {
	t.Parallel()
	if OneDay.Duration() != time.Hour*24 {
		t.Fatalf("unexpected result: %v", OneDay.Duration())
	}
}

func TestKlineShort(t *testing.T) {
	t.Parallel()
	assert.Equal(t, "24h", OneDay.Short(), "One day should show as 24h")
	assert.Equal(t, "1h", OneHour.Short(), "One hour should truncate 0m0s suffix")
	assert.Equal(t, "raw", Raw.Short(), "Raw should return raw")
}

func TestDurationToWord(t *testing.T) {
	t.Parallel()
	for _, tc := range []struct {
		name     string
		interval Interval
	}{
		{"raw", Raw},
		{"tenmillisec", TenMilliseconds},
		{"twentymillisec", TwentyMilliseconds},
		{"hundredmillisec", HundredMilliseconds},
		{"twohundredfiftymillisec", TwoHundredAndFiftyMilliseconds},
		{"thousandmillisec", ThousandMilliseconds},
		{"tensec", TenSecond},
		{"fifteensecond", FifteenSecond},
		{"onemin", OneMin},
		{"threemin", ThreeMin},
		{"fivemin", FiveMin},
		{"tenmin", TenMin},
		{"fifteenmin", FifteenMin},
		{"thirtymin", ThirtyMin},
		{"onehour", OneHour},
		{"twohour", TwoHour},
		{"fourhour", FourHour},
		{"sixhour", SixHour},
		{"eighthour", OneHour * 8},
		{"twelvehour", TwelveHour},
		{"oneday", OneDay},
		{"threeday", ThreeDay},
		{"fiveday", FiveDay},
		{"fifteenday", FifteenDay},
		{"oneweek", OneWeek},
		{"twoweek", TwoWeek},
		{"onemonth", OneMonth},
		{"threemonth", ThreeMonth},
		{"sixmonth", SixMonth},
		{"oneyear", OneYear},
		{"notfound", Interval(time.Hour * 1337)},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, tc.name, strings.ToLower(tc.interval.Word()))
		})
	}
}

func TestTotalCandlesPerInterval(t *testing.T) {
	t.Parallel()

	tmNow := time.Now()
	assert.Equal(t, uint64(0), TotalCandlesPerInterval(tmNow.AddDate(0, 0, 1), tmNow, OneMin))

	start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)

	for _, tc := range []struct {
		interval Interval
		expected uint64
	}{
		{
			FifteenSecond,
			2102400,
		},
		{
			OneMin,
			525600,
		},
		{
			ThreeMin,
			175200,
		},
		{
			FiveMin,
			105120,
		},
		{
			TenMin,
			52560,
		},
		{
			FifteenMin,
			35040,
		},
		{
			ThirtyMin,
			17520,
		},
		{
			OneHour,
			8760,
		},
		{
			TwoHour,
			4380,
		},
		{
			FourHour,
			2190,
		},
		{
			SixHour,
			1460,
		},
		{
			OneHour * 8,
			1095,
		},
		{
			TwelveHour,
			730,
		},
		{
			OneDay,
			365,
		},
		{
			ThreeDay,
			121,
		},
		{
			FiveDay,
			73,
		},
		{
			FifteenDay,
			24,
		},
		{
			OneWeek,
			52,
		},
		{
			TwoWeek,
			26,
		},
		{
			OneMonth,
			12,
		},
		{
			OneYear,
			1,
		},
	} {
		t.Run(tc.interval.String(), func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, tc.expected, TotalCandlesPerInterval(start, end, tc.interval))
		})
	}
}

func TestCalculateCandleDateRanges(t *testing.T) {
	t.Parallel()
	pt := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
	ft := time.Date(2222, 1, 1, 0, 0, 0, 0, time.UTC)
	et := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)

	_, err := CalculateCandleDateRanges(time.Time{}, time.Time{}, OneMin, 300)
	assert.ErrorIs(t, err, common.ErrDateUnset)

	_, err = CalculateCandleDateRanges(et, pt, OneMin, 300)
	assert.ErrorIs(t, err, common.ErrStartAfterEnd)

	_, err = CalculateCandleDateRanges(et, ft, 0, 300)
	assert.ErrorIs(t, err, ErrInvalidInterval)

	_, err = CalculateCandleDateRanges(et, et, OneMin, 300)
	assert.ErrorIs(t, err, common.ErrStartEqualsEnd)

	v, err := CalculateCandleDateRanges(pt, et, OneWeek, 300)
	require.NoError(t, err)
	assert.Equal(t, int64(1546214400), v.Ranges[0].Start.Ticks)

	v, err = CalculateCandleDateRanges(pt, et, OneWeek, 100)
	require.NoError(t, err)
	assert.Equal(t, 1, len(v.Ranges))
	assert.Equal(t, 52, len(v.Ranges[0].Intervals))

	v, err = CalculateCandleDateRanges(et, ft, OneWeek, 5)
	require.NoError(t, err)
	assert.Equal(t, 2108, len(v.Ranges))
	assert.Equal(t, 5, len(v.Ranges[0].Intervals))
	lenRanges := len(v.Ranges) - 1
	lenIntervals := len(v.Ranges[lenRanges].Intervals) - 1
	assert.True(t, v.Ranges[lenRanges].Intervals[lenIntervals].End.Equal(ft.Round(OneWeek.Duration())))

	start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
	end := start.Add(24 * time.Hour)
	v, err = CalculateCandleDateRanges(start, end, OneDay, 0)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), v.Limit)
}

func TestItem_SortCandlesByTimestamp(t *testing.T) {
	t.Parallel()
	tempKline := Item{
		Exchange: "testExchange",
		Pair:     currency.NewBTCUSDT(),
		Asset:    asset.Spot,
		Interval: OneDay,
	}

	for x := range 100 {
		y := rand.Float64() //nolint:gosec // used for generating test data, no need to import crypo/rand
		tempKline.Candles = append(tempKline.Candles,
			Candle{
				Time:   time.Now().AddDate(0, 0, -x),
				Open:   y,
				High:   y + float64(x),
				Low:    y - float64(x),
				Close:  y,
				Volume: y,
			})
	}

	tempKline.SortCandlesByTimestamp(false)
	if tempKline.Candles[0].Time.After(tempKline.Candles[1].Time) {
		t.Fatal("expected kline.Candles to be in descending order")
	}

	tempKline.SortCandlesByTimestamp(true)
	if tempKline.Candles[0].Time.Before(tempKline.Candles[1].Time) {
		t.Fatal("expected kline.Candles to be in ascending order")
	}
}

func setupTest(t *testing.T) {
	t.Helper()
	if verbose {
		err := testhelpers.EnableVerboseTestOutput()
		require.NoError(t, err, "EnableVerboseTestOutput must not error")
	}

	testhelpers.MigrationDir = filepath.Join("..", "..", "database", "migrations")
	testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()
	testhelpers.TempDir = t.TempDir()
}

func TestStoreInDatabase(t *testing.T) {
	setupTest(t)

	for _, tc := range []struct {
		name   string
		config *database.Config
		seedDB func(bool) error
		runner func(t *testing.T)
		closer func(dbConn *database.Instance) error
	}{
		{
			name:   "postgresql",
			config: testhelpers.PostgresTestDatabase,
			seedDB: seedDB,
		},
		{
			name: "SQLite",
			config: &database.Config{
				Driver:            database.DBSQLite3,
				ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"},
			},
			seedDB: seedDB,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if !testhelpers.CheckValidConfig(&tc.config.ConnectionDetails) {
				t.Skip("database not configured skipping test")
			}

			dbConn, err := testhelpers.ConnectToDatabase(tc.config)
			require.NoError(t, err)

			if tc.seedDB != nil {
				require.NoError(t, tc.seedDB(false))
			}

			_, ohlcvData, err := genOHCLVData()
			require.NoError(t, err)
			r, err := StoreInDatabase(&ohlcvData, false)
			require.NoError(t, err)
			assert.Equal(t, uint64(365), r)

			r, err = StoreInDatabase(&ohlcvData, true)
			require.NoError(t, err)
			assert.Equal(t, uint64(365), r)

			err = testhelpers.CloseDatabase(dbConn)
			assert.NoError(t, err)
		})
	}

	err := os.RemoveAll(testhelpers.TempDir)
	if err != nil {
		t.Fatalf("Failed to remove temp db file: %v", err)
	}
}

func TestLoadFromDatabase(t *testing.T) {
	setupTest(t)

	for _, tc := range []struct {
		name   string
		config *database.Config
		seedDB func(bool) error
		runner func(t *testing.T)
		closer func(dbConn *database.Instance) error
	}{
		{
			name:   "postgresql",
			config: testhelpers.PostgresTestDatabase,
			seedDB: seedDB,
		},
		{
			name: "SQLite",
			config: &database.Config{
				Driver:            database.DBSQLite3,
				ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"},
			},
			seedDB: seedDB,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if !testhelpers.CheckValidConfig(&tc.config.ConnectionDetails) {
				t.Skip("database not configured skipping test")
			}

			dbConn, err := testhelpers.ConnectToDatabase(tc.config)
			require.NoError(t, err)

			if tc.seedDB != nil {
				require.NoError(t, tc.seedDB(true))
			}
			start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
			end := start.AddDate(1, 0, 0)
			ret, err := LoadFromDatabase(testExchanges[0].Name, currency.NewBTCUSDT(), asset.Spot, OneDay, start, end)
			require.NoError(t, err)
			assert.Equal(t, ret.Exchange, testExchanges[0].Name)

			err = testhelpers.CloseDatabase(dbConn)
			assert.NoError(t, err)
		})
	}

	err := os.RemoveAll(testhelpers.TempDir)
	if err != nil {
		t.Fatalf("Failed to remove temp db file: %v", err)
	}
}

// TODO: find a better way to handle this to remove duplication between candle test
func seedDB(includeOHLCVData bool) error {
	err := exchange.InsertMany(testExchanges)
	if err != nil {
		return err
	}

	if includeOHLCVData {
		data, _, err := genOHCLVData()
		if err != nil {
			return err
		}
		_, err = candle.Insert(&data)
		return err
	}
	return nil
}

func genOHCLVData() (out candle.Item, outItem Item, err error) {
	exchangeUUID, err := exchange.UUIDByName(testExchanges[0].Name)
	if err != nil {
		return
	}

	out.ExchangeID = exchangeUUID.String()
	out.Base = currency.BTC.String()
	out.Quote = currency.USDT.String()
	out.Interval = 86400
	out.Asset = "spot"

	start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
	for x := range 365 {
		out.Candles = append(out.Candles, candle.Candle{
			Timestamp: start.Add(time.Hour * 24 * time.Duration(x)),
			Open:      1000,
			High:      1000,
			Low:       1000,
			Close:     1000,
			Volume:    1000,
		})
	}

	outItem.Interval = OneDay
	outItem.Asset = asset.Spot
	outItem.Pair = currency.NewBTCUSDT()
	outItem.Exchange = testExchanges[0].Name

	for x := range 365 {
		outItem.Candles = append(outItem.Candles, Candle{
			Time:   start.Add(time.Hour * 24 * time.Duration(x)),
			Open:   1000,
			High:   1000,
			Low:    1000,
			Close:  1000,
			Volume: 1000,
		})
	}

	return out, outItem, nil
}

func TestLoadCSV(t *testing.T) {
	v, err := LoadFromGCTScriptCSV(filepath.Join("..", "..", "testdata", "binance_BTCUSDT_24h_2019_01_01_2020_01_01.csv"))
	if err != nil {
		t.Fatal(err)
	}

	if v[0].Time.UTC() != time.Unix(1546300800, 0).UTC() {
		t.Fatalf("unexpected value received: %v", v[0].Time)
	}

	if v[269].Close != 8177.91 {
		t.Fatalf("unexpected value received: %v", v[269].Close)
	}

	if v[364].Open != 7246 {
		t.Fatalf("unexpected value received: %v", v[364].Open)
	}
}

func TestVerifyResultsHaveData(t *testing.T) {
	t.Parallel()
	tt1 := time.Now().Round(OneDay.Duration())
	tt2 := tt1.Add(OneDay.Duration())
	tt3 := tt2.Add(OneDay.Duration()) // end date no longer inclusive
	dateRanges, err := CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
	assert.NoError(t, err)

	if dateRanges.HasDataAtDate(tt1) {
		t.Error("unexpected true value")
	}
	err = dateRanges.SetHasDataFromCandles([]Candle{
		{
			Time: tt1,
			Low:  1337,
		},
		{
			Time: tt2,
		},
	})
	assert.NoError(t, err)

	if !dateRanges.HasDataAtDate(tt1) {
		t.Error("expected true")
	}
	err = dateRanges.SetHasDataFromCandles([]Candle{
		{
			Time: tt1,
		},
		{
			Time: tt2,
			Low:  1337,
		},
	})
	assert.NoError(t, err)

	if dateRanges.HasDataAtDate(tt1) {
		t.Error("expected false")
	}
}

func TestDataSummary(t *testing.T) {
	t.Parallel()
	tt1 := time.Now().Add(-time.Hour * 24).Round(OneDay.Duration())
	tt2 := time.Now().Round(OneDay.Duration())
	tt3 := time.Now().Add(time.Hour * 24).Round(OneDay.Duration())
	dateRanges, err := CalculateCandleDateRanges(tt1, tt2, OneDay, 0)
	assert.NoError(t, err)

	result := dateRanges.DataSummary(false)
	if len(result) != 1 {
		t.Errorf("expected %v received %v", 1, len(result))
	}
	dateRanges, err = CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
	assert.NoError(t, err)

	dateRanges.Ranges[0].Intervals[0].HasData = true
	result = dateRanges.DataSummary(true)
	if len(result) != 2 {
		t.Errorf("expected %v received %v", 2, len(result))
	}
	result = dateRanges.DataSummary(false)
	if len(result) != 1 {
		t.Errorf("expected %v received %v", 1, len(result))
	}
}

func TestHasDataAtDate(t *testing.T) {
	t.Parallel()
	tt1 := time.Now().Round(OneDay.Duration())
	tt2 := tt1.Add(OneDay.Duration())
	tt3 := tt2.Add(OneDay.Duration()) // end date no longer inclusive
	dateRanges, err := CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
	assert.NoError(t, err)

	if dateRanges.HasDataAtDate(tt2) {
		t.Error("unexpected true value")
	}

	err = dateRanges.SetHasDataFromCandles([]Candle{
		{
			Time:  tt1,
			Close: 1337,
		},
		{
			Time:  tt2,
			Close: 1337,
		},
	})
	assert.NoError(t, err)

	if !dateRanges.HasDataAtDate(tt2) {
		t.Error("unexpected false value")
	}

	if dateRanges.HasDataAtDate(tt2.Add(time.Hour * 24)) {
		t.Error("should not have data")
	}
}

func TestIntervalsPerYear(t *testing.T) {
	t.Parallel()
	var i Interval
	if i.IntervalsPerYear() != 0 {
		t.Error("expected 0")
	}
	i = OneYear
	if i.IntervalsPerYear() != 1.0 {
		t.Error("expected 1")
	}
	i = OneDay
	if i.IntervalsPerYear() != 365 {
		t.Error("expected 365")
	}
	i = OneHour
	if i.IntervalsPerYear() != 8760 {
		t.Error("expected 8670")
	}
	i = TwoHour
	if i.IntervalsPerYear() != 4380 {
		t.Error("expected 4380")
	}
	i = TwoHour + FifteenSecond
	if i.IntervalsPerYear() != 4370.893970893971 {
		t.Error("expected 4370...")
	}
}

// The purpose of this benchmark is to highlight that requesting
// '.Unix()` frequently is a slow process
func BenchmarkJustifyIntervalTimeStoringUnixValues1(b *testing.B) {
	tt1 := time.Now()
	tt2 := time.Now().Add(-time.Hour)
	tt3 := time.Now().Add(time.Hour)
	for b.Loop() {
		if tt1.Unix() == tt2.Unix() || (tt1.Unix() > tt2.Unix() && tt1.Unix() < tt3.Unix()) {
			continue
		}
	}
}

// The purpose of this benchmark is to highlight that storing the unix value
// at time of creation is dramatically faster than frequently requesting `.Unix()`
// at runtime at scale. When dealing with the backtester and comparing
// tens of thousands of candle times
func BenchmarkJustifyIntervalTimeStoringUnixValues2(b *testing.B) {
	tt1 := time.Now().Unix()
	tt2 := time.Now().Add(-time.Hour).Unix()
	tt3 := time.Now().Add(time.Hour).Unix()
	for b.Loop() {
		if tt1 >= tt2 && tt1 <= tt3 {
			continue
		}
	}
}

func TestConvertToNewInterval(t *testing.T) {
	_, err := (*Item)(nil).ConvertToNewInterval(OneMin)
	assert.ErrorIs(t, err, errNilKline)

	_, err = (&Item{}).ConvertToNewInterval(OneMin)
	assert.ErrorIs(t, err, ErrInvalidInterval)

	old := &Item{
		Exchange: "lol",
		Pair:     currency.NewBTCUSDT(),
		Asset:    asset.Spot,
		Interval: OneDay,
		Candles: []Candle{
			{
				Time:   time.Now(),
				Open:   1337,
				High:   1339,
				Low:    1336,
				Close:  1338,
				Volume: 1337,
			},
			{
				Time:   time.Now().AddDate(0, 0, 1),
				Open:   1338,
				High:   2000,
				Low:    1332,
				Close:  1696,
				Volume: 6420,
			},
			{
				Time:   time.Now().AddDate(0, 0, 2),
				Open:   1696,
				High:   1998,
				Low:    1337,
				Close:  6969,
				Volume: 2520,
			},
		},
	}

	_, err = old.ConvertToNewInterval(0)
	assert.ErrorIs(t, err, ErrInvalidInterval)

	_, err = old.ConvertToNewInterval(OneMin)
	assert.ErrorIs(t, err, ErrCanOnlyUpscaleCandles)

	old.Interval = ThreeDay
	_, err = old.ConvertToNewInterval(OneWeek)
	assert.ErrorIs(t, err, ErrWholeNumberScaling)

	old.Interval = OneDay
	newInterval := ThreeDay
	newCandle, err := old.ConvertToNewInterval(newInterval)
	require.NoError(t, err)

	if len(newCandle.Candles) != 1 {
		t.Error("expected one candle")
	}
	if newCandle.Candles[0].Open != 1337 &&
		newCandle.Candles[0].High != 2000 &&
		newCandle.Candles[0].Low != 1332 &&
		newCandle.Candles[0].Close != 6969 &&
		newCandle.Candles[0].Volume != (2520+6420+1337) {
		t.Error("unexpected updoot")
	}

	old.Candles = append(old.Candles, Candle{
		Time:   time.Now().AddDate(0, 0, 3),
		Open:   6969,
		High:   1998,
		Low:    2342,
		Close:  7777,
		Volume: 111,
	})
	newCandle, err = old.ConvertToNewInterval(newInterval)
	assert.NoError(t, err)

	if len(newCandle.Candles) != 1 {
		t.Error("expected one candle")
	}

	_, err = old.ConvertToNewInterval(OneMonth)
	assert.ErrorIs(t, err, ErrInsufficientCandleData)

	tn := time.Now().Truncate(time.Duration(OneDay))

	// Test incorrectly padded candles
	old.Candles = []Candle{
		{
			Time:   tn,
			Open:   1337,
			High:   1339,
			Low:    1336,
			Close:  1338,
			Volume: 1337,
		},
		{
			Time:   tn.AddDate(0, 0, 1),
			Open:   1338,
			High:   2000,
			Low:    1332,
			Close:  1696,
			Volume: 6420,
		},
		{
			Time:   tn.AddDate(0, 0, 2),
			Open:   1696,
			High:   1998,
			Low:    1337,
			Close:  6969,
			Volume: 2520,
		},
		// empty candle should be here <---
		// aaaand empty candle should be here <---
		{
			Time:   tn.AddDate(0, 0, 5),
			Open:   6969,
			High:   8888,
			Low:    1111,
			Close:  5555,
			Volume: 2520,
		},
		{
			Time: tn.AddDate(0, 0, 6),
			// Empty end padding
		},
		{
			Time: tn.AddDate(0, 0, 7),
			// Empty end padding
		},
		{
			Time: tn.AddDate(0, 0, 8),
			// Empty end padding
		},
	}

	_, err = old.ConvertToNewInterval(newInterval)
	assert.ErrorIs(t, err, errCandleDataNotPadded)

	err = old.addPadding(tn, tn.AddDate(0, 0, 9), false)
	require.NoError(t, err)

	newCandle, err = old.ConvertToNewInterval(newInterval)
	require.NoError(t, err)

	if len(newCandle.Candles) != 3 {
		t.Errorf("received '%v' expected '%v'", len(newCandle.Candles), 3)
	}
}

func TestAddPadding(t *testing.T) {
	t.Parallel()

	tn := time.Now().Truncate(time.Duration(OneDay))

	var k *Item
	err := k.addPadding(tn, tn.AddDate(0, 0, 5), false)
	require.ErrorIs(t, err, errNilKline)

	k = &Item{}
	k.Candles = []Candle{
		{
			Time:   tn,
			Open:   1337,
			High:   1339,
			Low:    1336,
			Close:  1338,
			Volume: 1337,
		},
	}
	err = k.addPadding(tn, tn.AddDate(0, 0, 5), false)
	require.ErrorIs(t, err, ErrInvalidInterval)

	k.Interval = OneDay
	k.Candles = []Candle{
		{
			Time:   tn.AddDate(0, 0, 1),
			Open:   1338,
			High:   2000,
			Low:    1332,
			Close:  1696,
			Volume: 6420,
		},
		{
			Time:   tn,
			Open:   1337,
			High:   1339,
			Low:    1336,
			Close:  1338,
			Volume: 1337,
		},
	}
	err = k.addPadding(tn.AddDate(0, 0, 5), tn, false)
	require.ErrorIs(t, err, errCannotEstablishTimeWindow)

	k.Candles = []Candle{
		{
			Time:   tn.Add(time.Hour * 8),
			Open:   1337,
			High:   1339,
			Low:    1336,
			Close:  1338,
			Volume: 1337,
		},
		{
			Time:   tn.AddDate(0, 0, 1).Add(time.Hour * 8),
			Open:   1338,
			High:   2000,
			Low:    1332,
			Close:  1696,
			Volume: 6420,
		},
		{
			Time:   tn.AddDate(0, 0, 2).Add(time.Hour * 8),
			Open:   1696,
			High:   1998,
			Low:    1337,
			Close:  6969,
			Volume: 2520,
		},
	}

	err = k.addPadding(tn, tn.AddDate(0, 0, 3), false)
	require.ErrorIs(t, err, errCandleOpenTimeIsNotUTCAligned)

	k.Candles = []Candle{
		{
			Time:   tn,
			Open:   1337,
			High:   1339,
			Low:    1336,
			Close:  1338,
			Volume: 1337,
		},
		{
			Time:   tn.AddDate(0, 0, 1),
			Open:   1338,
			High:   2000,
			Low:    1332,
			Close:  1696,
			Volume: 6420,
		},
		{
			Time:   tn.AddDate(0, 0, 2),
			Open:   1696,
			High:   1998,
			Low:    1337,
			Close:  6969,
			Volume: 2520,
		},
	}

	err = k.addPadding(tn, tn.AddDate(0, 0, 3), false)
	require.NoError(t, err)

	if len(k.Candles) != 3 {
		t.Fatalf("received '%v' expected '%v'", len(k.Candles), 3)
	}

	k.Candles = append(k.Candles, Candle{
		Time:   tn.AddDate(0, 0, 5),
		Open:   6969,
		High:   8888,
		Low:    1111,
		Close:  5555,
		Volume: 2520,
	})

	err = k.addPadding(tn, tn.AddDate(0, 0, 6), false)
	require.NoError(t, err)

	if len(k.Candles) != 6 {
		t.Fatalf("received '%v' expected '%v'", len(k.Candles), 6)
	}

	// No candles test when there is zero activity for that period
	k.Candles = nil

	err = k.addPadding(tn, tn.AddDate(0, 0, 6), false)
	assert.NoError(t, err)

	if len(k.Candles) != 6 {
		t.Errorf("received '%v' expected '%v'", len(k.Candles), 6)
	}
}

func TestGetClosePriceAtTime(t *testing.T) {
	t.Parallel()
	tt := time.Now()
	k := Item{
		Candles: []Candle{
			{
				Time:  tt,
				Close: 1337,
			},
			{
				Time:  tt.Add(time.Hour),
				Close: 1338,
			},
		},
	}
	price, err := k.GetClosePriceAtTime(tt)
	assert.NoError(t, err)

	if price != 1337 {
		t.Errorf("received '%v' expected '%v'", price, 1337)
	}
	_, err = k.GetClosePriceAtTime(tt.Add(time.Minute))
	assert.ErrorIs(t, err, ErrNotFoundAtTime)
}

func TestDeployExchangeIntervals(t *testing.T) {
	t.Parallel()
	exchangeIntervals := DeployExchangeIntervals()
	if exchangeIntervals.ExchangeSupported(OneWeek) {
		t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), false)
	}

	exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek})
	if !exchangeIntervals.ExchangeSupported(OneWeek) {
		t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), true)
	}

	_, err := exchangeIntervals.Construct(0)
	assert.ErrorIs(t, err, ErrInvalidInterval)

	_, err = exchangeIntervals.Construct(OneMin)
	assert.ErrorIs(t, err, ErrCannotConstructInterval)

	request, err := exchangeIntervals.Construct(OneWeek)
	assert.NoError(t, err)

	if request != OneWeek {
		t.Errorf("received '%v' expected '%v'", request, OneWeek)
	}

	exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek}, IntervalCapacity{Interval: OneDay})

	request, err = exchangeIntervals.Construct(OneMonth)
	assert.NoError(t, err)

	if request != OneDay {
		t.Errorf("received '%v' expected '%v'", request, OneDay)
	}
}

func TestSetHasDataFromCandles(t *testing.T) {
	t.Parallel()
	ohc := getOneHour()
	localEnd := ohc[len(ohc)-1].Time.Add(OneHour.Duration())
	i, err := CalculateCandleDateRanges(ohc[0].Time, localEnd, OneHour, 100000)
	assert.NoError(t, err)

	err = i.SetHasDataFromCandles(ohc)
	assert.NoError(t, err)

	if !i.Start.Equal(ohc[0].Time) {
		t.Errorf("received '%v' expected '%v'", i.Start.Time, ohc[0].Time)
	}
	if !i.End.Equal(localEnd) {
		t.Errorf("received '%v' expected '%v'", i.End.Time, ohc[len(ohc)-1].Time)
	}

	k := Item{
		Interval: OneHour,
		Candles:  ohc[2:],
	}
	err = k.addPadding(i.Start.Time, i.End.Time, false)
	assert.NoError(t, err)

	err = i.SetHasDataFromCandles(k.Candles)
	assert.NoError(t, err)

	if !i.Start.Equal(k.Candles[0].Time) {
		t.Errorf("received '%v' expected '%v'", i.Start.Time, k.Candles[0].Time)
	}
	if i.HasDataAtDate(k.Candles[0].Time) {
		t.Errorf("received '%v' expected '%v'", false, true)
	}
	if !i.HasDataAtDate(k.Candles[len(k.Candles)-1].Time) {
		t.Errorf("received '%v' expected '%v'", true, false)
	}
}

func TestGetIntervalResultLimit(t *testing.T) {
	t.Parallel()

	var e *ExchangeCapabilitiesEnabled
	_, err := e.GetIntervalResultLimit(OneMin)
	assert.ErrorIs(t, err, errExchangeCapabilitiesEnabledIsNil)

	e = &ExchangeCapabilitiesEnabled{}
	e.Intervals = ExchangeIntervals{}
	_, err = e.GetIntervalResultLimit(OneDay)
	assert.ErrorIs(t, err, errIntervalNotSupported)

	e.Intervals = ExchangeIntervals{
		supported: map[Interval]uint64{
			OneDay: 100000,
			OneMin: 0,
		},
	}

	_, err = e.GetIntervalResultLimit(OneMin)
	assert.ErrorIs(t, err, errCannotFetchIntervalLimit)

	limit, err := e.GetIntervalResultLimit(OneDay)
	assert.NoError(t, err)

	if limit != 100000 {
		t.Errorf("received '%v' expected '%v'", limit, 100000)
	}

	e.GlobalResultLimit = 1337
	limit, err = e.GetIntervalResultLimit(OneMin)
	assert.NoError(t, err)

	if limit != 1337 {
		t.Errorf("received '%v' expected '%v'", limit, 1337)
	}
}

func TestUnmarshalJSON(t *testing.T) {
	t.Parallel()
	var i Interval
	for _, tt := range []struct {
		in  string
		exp Interval
	}{{`"3m"`, ThreeMin}, {`"15s"`, FifteenSecond}, {`720000000000`, OneMin * 12}, {`"-1ns"`, Raw}, {`"raw"`, Raw}} {
		err := i.UnmarshalJSON([]byte(tt.in))
		assert.NoErrorf(t, err, "UnmarshalJSON should not error on %q", tt.in)
	}
	err := i.UnmarshalJSON([]byte(`"6hedgehogs"`))
	assert.ErrorContains(t, err, "unknown unit", "UnmarshalJSON should error")
}
