package history

import (
	"math/big"
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/ethereum/go-ethereum/common"
	"github.com/status-im/status-go/t/helpers"
	"github.com/status-im/status-go/walletdatabase"
)

func newTestDB(t *testing.T) *BalanceDB {
	db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
	require.NoError(t, err)
	return NewBalanceDB(db)
}

func dbWithEntries(t *testing.T, entries []*entry) *BalanceDB {
	db := newTestDB(t)
	for _, entry := range entries {
		err := db.add(entry)
		require.NoError(t, err)
	}
	return db
}

func TestBalance_addPaddingPoints(t *testing.T) {
	type args struct {
		currency         string
		addresses        []common.Address
		fromTimestamp    uint64
		currentTimestamp uint64
		data             []*entry
		limit            int
	}
	tests := []struct {
		name    string
		args    args
		want    []*entry
		wantErr bool
	}{
		{
			name: "addOnePaddingPointAtMiddle",
			args: args{
				currency:         "ETH",
				addresses:        []common.Address{common.Address{1}},
				fromTimestamp:    0,
				currentTimestamp: 2,
				data: []*entry{
					{
						balance:     big.NewInt(0),
						timestamp:   0,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(2),
						timestamp:   2,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
				},
				limit: 3,
			},
			want: []*entry{
				{
					balance:     big.NewInt(0),
					timestamp:   0,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(2),
					timestamp:   2,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "noPaddingEqualsLimit",
			args: args{
				currency:         "ETH",
				addresses:        []common.Address{common.Address{1}},
				fromTimestamp:    0,
				currentTimestamp: 2,
				data: []*entry{
					{
						balance:     big.NewInt(0),
						timestamp:   0,
						block:       big.NewInt(1),
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(1),
						timestamp:   2,
						block:       big.NewInt(2),
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
				},
				limit: 2,
			},
			want: []*entry{
				{
					balance:     big.NewInt(0),
					timestamp:   0,
					block:       big.NewInt(1),
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(1),
					timestamp:   2,
					block:       big.NewInt(2),
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "limitLessThanDataSize",
			args: args{
				currency:         "ETH",
				addresses:        []common.Address{common.Address{1}},
				fromTimestamp:    0,
				currentTimestamp: 2,
				data: []*entry{
					{
						balance:     big.NewInt(0),
						timestamp:   0,
						block:       big.NewInt(1),
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(1),
						timestamp:   2,
						block:       big.NewInt(2),
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
				},
				limit: 1,
			},
			want: []*entry{
				{
					balance:     big.NewInt(0),
					timestamp:   0,
					block:       big.NewInt(1),
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(1),
					timestamp:   2,
					block:       big.NewInt(2),
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "addMultiplePaddingPoints",
			args: args{
				currency:         "ETH",
				addresses:        []common.Address{common.Address{1}},
				fromTimestamp:    1,
				currentTimestamp: 5,
				data: []*entry{
					{
						balance:     big.NewInt(0),
						timestamp:   1,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(4),
						timestamp:   4,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(5),
						timestamp:   5,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
				},
				limit: 5,
			},
			want: []*entry{
				{
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(0),
					timestamp:   2,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(0),
					timestamp:   3,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(4),
					timestamp:   4,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(5),
					timestamp:   5,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "addMultiplePaddingPointsDuplicateTimestamps",
			args: args{
				currency:         "ETH",
				addresses:        []common.Address{common.Address{1}},
				fromTimestamp:    1,
				currentTimestamp: 5,
				data: []*entry{
					{
						balance:     big.NewInt(0),
						timestamp:   1,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(0),
						timestamp:   1,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(4),
						timestamp:   4,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
					{
						balance:     big.NewInt(5),
						timestamp:   5,
						tokenSymbol: "ETH",
						address:     common.Address{1},
					},
				},
				limit: 5,
			},
			want: []*entry{
				{
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(0),
					timestamp:   2,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(4),
					timestamp:   4,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
				{
					balance:     big.NewInt(5),
					timestamp:   5,
					tokenSymbol: "ETH",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotRes, err := addPaddingPoints(tt.args.currency, tt.args.addresses, tt.args.currentTimestamp, tt.args.data, tt.args.limit)
			if (err != nil) != tt.wantErr {
				t.Errorf("Balance.addPaddingPoints() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(gotRes, tt.want) {
				t.Errorf("Balance.addPaddingPoints() = %v, want %v", gotRes, tt.want)
			}
		})
	}
}

func TestBalance_addEdgePoints(t *testing.T) {

	walletDB := newTestDB(t)

	type fields struct {
		db *BalanceDB
	}
	type args struct {
		chainID       uint64
		currency      string
		addresses     []common.Address
		fromTimestamp uint64
		toTimestamp   uint64
		data          []*entry
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantRes []*entry
		wantErr bool
	}{
		{
			name: "addToEmptyData",
			fields: fields{
				db: walletDB,
			},
			args: args{
				chainID:       111,
				currency:      "SNT",
				addresses:     []common.Address{common.Address{1}},
				fromTimestamp: 1,
				toTimestamp:   2,
				data:          []*entry{},
			},
			wantRes: []*entry{
				{
					chainID:     111,
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
				{
					chainID:     111,
					balance:     big.NewInt(0),
					timestamp:   2,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "addToEmptyDataSinceGenesis",
			fields: fields{
				db: walletDB,
			},
			args: args{
				chainID:       111,
				currency:      "SNT",
				addresses:     []common.Address{common.Address{1}},
				fromTimestamp: 0, // will set to genesisTimestamp
				toTimestamp:   genesisTimestamp + 1,
				data:          []*entry{},
			},
			wantRes: []*entry{
				{
					chainID:     111,
					balance:     big.NewInt(0),
					timestamp:   genesisTimestamp,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
				{
					chainID:     111,
					balance:     big.NewInt(0),
					timestamp:   genesisTimestamp + 1,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "addToNonEmptyDataFromPreviousEntry",
			fields: fields{
				db: dbWithEntries(t, []*entry{
					{
						chainID:     111,
						balance:     big.NewInt(1),
						timestamp:   1,
						block:       big.NewInt(1),
						tokenSymbol: "SNT",
						address:     common.Address{1},
					},
				}),
			},
			args: args{
				chainID:       111,
				currency:      "SNT",
				addresses:     []common.Address{common.Address{1}},
				fromTimestamp: 2,
				toTimestamp:   4,
				data: []*entry{
					{
						chainID:     111,
						balance:     big.NewInt(3),
						timestamp:   3,
						block:       big.NewInt(3),
						tokenSymbol: "SNT",
						address:     common.Address{1},
					},
					{
						chainID:     111,
						balance:     big.NewInt(2),
						timestamp:   4,
						block:       big.NewInt(4),
						tokenSymbol: "SNT",
						address:     common.Address{1},
					},
				},
			},
			wantRes: []*entry{
				{
					chainID:     111,
					balance:     big.NewInt(1),
					timestamp:   2,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
				{
					chainID:     111,
					balance:     big.NewInt(3),
					timestamp:   3,
					block:       big.NewInt(3),
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
				{
					chainID:     111,
					balance:     big.NewInt(2),
					timestamp:   4,
					block:       big.NewInt(4),
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
		{
			name: "addToNonEmptyData",
			fields: fields{
				db: walletDB,
			},
			args: args{
				chainID:       111,
				currency:      "SNT",
				addresses:     []common.Address{common.Address{1}},
				fromTimestamp: 1,
				toTimestamp:   2,
				data: []*entry{
					{
						chainID:     111,
						balance:     big.NewInt(2),
						timestamp:   2,
						block:       big.NewInt(2),
						tokenSymbol: "SNT",
						address:     common.Address{1},
					},
				},
			},
			wantRes: []*entry{
				{
					chainID:     111,
					balance:     big.NewInt(0),
					timestamp:   1,
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
				{
					chainID:     111,
					balance:     big.NewInt(2),
					timestamp:   2,
					block:       big.NewInt(2),
					tokenSymbol: "SNT",
					address:     common.Address{1},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			b := &Balance{
				db: tt.fields.db,
			}
			gotRes, err := b.addEdgePoints(tt.args.chainID, tt.args.currency, tt.args.addresses, tt.args.fromTimestamp, tt.args.toTimestamp, tt.args.data)
			if (err != nil) != tt.wantErr {
				t.Errorf("Balance.addEdgePoints() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(gotRes, tt.wantRes) {
				t.Errorf("Balance.addEdgePoints() = \n%v,\nwant \n%v", gotRes, tt.wantRes)
			}
		})
	}
}