package orm

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	storetypes "cosmossdk.io/store/types"

	"git.cw.tr/mukan-network/mukan-sdk/codec"
	"git.cw.tr/mukan-network/mukan-sdk/codec/types"
	"git.cw.tr/mukan-network/mukan-sdk/testutil/testdata"
	"git.cw.tr/mukan-network/mukan-sdk/x/group/errors"
)

// Testing ORM with arbitrary metadata length
const metadataLen = 10

func TestKeeperEndToEndWithAutoUInt64Table(t *testing.T) {
	interfaceRegistry := types.NewInterfaceRegistry()
	cdc := codec.NewProtoCodec(interfaceRegistry)

	ctx := NewMockContext()
	store := ctx.KVStore(storetypes.NewKVStoreKey("test"))

	k := NewTestKeeper(cdc)

	tm := testdata.TableModel{
		Id:       1,
		Name:     "name",
		Number:   123,
		Metadata: []byte("metadata"),
	}
	// when stored
	rowID, err := k.autoUInt64Table.Create(store, &tm)
	require.NoError(t, err)
	// then we should find it
	exists := k.autoUInt64Table.Has(store, rowID)
	require.True(t, exists)

	// and load it
	var loaded testdata.TableModel

	binKey, err := k.autoUInt64Table.GetOne(store, rowID, &loaded)
	require.NoError(t, err)

	require.Equal(t, rowID, binary.BigEndian.Uint64(binKey))
	require.Equal(t, tm, loaded)

	// and exists in MultiKeyIndex
	exists, err = k.autoUInt64TableModelByMetadataIndex.Has(store, []byte("metadata"))
	require.NoError(t, err)
	require.True(t, exists)

	// and when loaded
	it, err := k.autoUInt64TableModelByMetadataIndex.Get(store, []byte("metadata"))
	require.NoError(t, err)

	// then
	binKey, loaded = first(t, it)
	assert.Equal(t, rowID, binary.BigEndian.Uint64(binKey))
	assert.Equal(t, tm, loaded)

	// when updated
	tm.Metadata = []byte("new-metadata")
	err = k.autoUInt64Table.Update(store, rowID, &tm)
	require.NoError(t, err)

	binKey, err = k.autoUInt64Table.GetOne(store, rowID, &loaded)
	require.NoError(t, err)

	require.Equal(t, rowID, binary.BigEndian.Uint64(binKey))
	require.Equal(t, tm, loaded)

	// then indexes are updated, too
	exists, err = k.autoUInt64TableModelByMetadataIndex.Has(store, []byte("new-metadata"))
	require.NoError(t, err)
	require.True(t, exists)

	exists, err = k.autoUInt64TableModelByMetadataIndex.Has(store, []byte("metadata"))
	require.NoError(t, err)
	require.False(t, exists)

	// when deleted
	err = k.autoUInt64Table.Delete(store, rowID)
	require.NoError(t, err)

	exists = k.autoUInt64Table.Has(store, rowID)
	require.False(t, exists)

	// and also removed from secondary MultiKeyIndex
	exists, err = k.autoUInt64TableModelByMetadataIndex.Has(store, []byte("new-metadata"))
	require.NoError(t, err)
	require.False(t, exists)
}

func TestKeeperEndToEndWithPrimaryKeyTable(t *testing.T) {
	interfaceRegistry := types.NewInterfaceRegistry()
	cdc := codec.NewProtoCodec(interfaceRegistry)

	ctx := NewMockContext()
	store := ctx.KVStore(storetypes.NewKVStoreKey("test"))

	k := NewTestKeeper(cdc)

	tm := testdata.TableModel{
		Id:       1,
		Name:     "name",
		Number:   123,
		Metadata: []byte("metadata"),
	}
	// when stored
	err := k.primaryKeyTable.Create(store, &tm)
	require.NoError(t, err)
	// then we should find it by primary key
	primaryKey := PrimaryKey(&tm)
	exists := k.primaryKeyTable.Has(store, primaryKey)
	require.True(t, exists)

	// and load it by primary key
	var loaded testdata.TableModel
	err = k.primaryKeyTable.GetOne(store, primaryKey, &loaded)
	require.NoError(t, err)

	// then values should match expectations
	require.Equal(t, tm, loaded)

	// and then the data should exists in MultiKeyIndex
	exists, err = k.primaryKeyTableModelByNumberIndex.Has(store, tm.Number)
	require.NoError(t, err)
	require.True(t, exists)

	// and when loaded from MultiKeyIndex
	it, err := k.primaryKeyTableModelByNumberIndex.Get(store, tm.Number)
	require.NoError(t, err)

	// then values should match as before
	_, err = First(it, &loaded)
	require.NoError(t, err)
	assert.Equal(t, tm, loaded)

	// and when we create another entry with the same primary key
	err = k.primaryKeyTable.Create(store, &tm)
	// then it should fail as the primary key must be unique
	require.True(t, errors.ErrORMUniqueConstraint.Is(err), err)

	// and when entity updated with new primary key
	updatedMember := &testdata.TableModel{
		Id:       2,
		Name:     tm.Name,
		Number:   tm.Number,
		Metadata: tm.Metadata,
	}
	// then it should fail as the primary key is immutable
	err = k.primaryKeyTable.Update(store, updatedMember)
	require.Error(t, err)

	// and when entity updated with non primary key attribute modified
	updatedMember = &testdata.TableModel{
		Id:       1,
		Name:     "new name",
		Number:   tm.Number,
		Metadata: tm.Metadata,
	}
	// then it should not fail
	err = k.primaryKeyTable.Update(store, updatedMember)
	require.NoError(t, err)

	// and when entity deleted
	err = k.primaryKeyTable.Delete(store, &tm)
	require.NoError(t, err)

	// it is removed from primaryKeyTable
	exists = k.primaryKeyTable.Has(store, primaryKey)
	require.False(t, exists)

	// and removed from secondary MultiKeyIndex
	exists, err = k.primaryKeyTableModelByNumberIndex.Has(store, tm.Number)
	require.NoError(t, err)
	require.False(t, exists)
}

func TestGasCostsPrimaryKeyTable(t *testing.T) {
	interfaceRegistry := types.NewInterfaceRegistry()
	cdc := codec.NewProtoCodec(interfaceRegistry)

	ctx := NewMockContext()
	store := ctx.KVStore(storetypes.NewKVStoreKey("test"))

	k := NewTestKeeper(cdc)

	tm := testdata.TableModel{
		Id:       1,
		Name:     "name",
		Number:   123,
		Metadata: []byte("metadata"),
	}
	rowID, err := k.autoUInt64Table.Create(store, &tm)
	require.NoError(t, err)
	require.Equal(t, uint64(1), rowID)

	gCtx := NewGasCountingMockContext()
	err = k.primaryKeyTable.Create(gCtx.KVStore(store), &tm)
	require.NoError(t, err)
	t.Logf("gas consumed on create: %d", gCtx.GasConsumed())

	// get by primary key
	gCtx.ResetGasMeter()
	var loaded testdata.TableModel
	err = k.primaryKeyTable.GetOne(gCtx.KVStore(store), PrimaryKey(&tm), &loaded)
	require.NoError(t, err)
	t.Logf("gas consumed on get by primary key: %d", gCtx.GasConsumed())

	// get by secondary index
	gCtx.ResetGasMeter()
	// and when loaded from MultiKeyIndex
	it, err := k.primaryKeyTableModelByNumberIndex.Get(gCtx.KVStore(store), tm.Number)
	require.NoError(t, err)
	var loadedSlice []testdata.TableModel
	_, err = ReadAll(it, &loadedSlice)
	require.NoError(t, err)
	t.Logf("gas consumed on get by multi index key: %d", gCtx.GasConsumed())

	// delete
	gCtx.ResetGasMeter()
	err = k.primaryKeyTable.Delete(gCtx.KVStore(store), &tm)
	require.NoError(t, err)
	t.Logf("gas consumed on delete by primary key: %d", gCtx.GasConsumed())

	// with 3 elements
	var tms []testdata.TableModel
	for i := 1; i < 4; i++ {
		gCtx.ResetGasMeter()
		tm := testdata.TableModel{
			Id:       uint64(i),
			Name:     fmt.Sprintf("name%d", i),
			Number:   123,
			Metadata: []byte("metadata"),
		}
		err = k.primaryKeyTable.Create(gCtx.KVStore(store), &tm)
		require.NoError(t, err)
		t.Logf("%d: gas consumed on create: %d", i, gCtx.GasConsumed())
		tms = append(tms, tm)
	}

	for i := 1; i < 4; i++ {
		gCtx.ResetGasMeter()
		tm := testdata.TableModel{
			Id:       uint64(i),
			Name:     fmt.Sprintf("name%d", i),
			Number:   123,
			Metadata: []byte("metadata"),
		}
		err = k.primaryKeyTable.GetOne(gCtx.KVStore(store), PrimaryKey(&tm), &loaded)
		require.NoError(t, err)
		t.Logf("%d: gas consumed on get by primary key: %d", i, gCtx.GasConsumed())
	}

	// get by secondary index
	gCtx.ResetGasMeter()
	// and when loaded from MultiKeyIndex
	it, err = k.primaryKeyTableModelByNumberIndex.Get(gCtx.KVStore(store), tm.Number)
	require.NoError(t, err)
	_, err = ReadAll(it, &loadedSlice)
	require.NoError(t, err)
	require.Len(t, loadedSlice, 3)
	t.Logf("gas consumed on get by multi index key: %d", gCtx.GasConsumed())

	// delete
	for i, m := range tms {
		gCtx.ResetGasMeter()

		err = k.primaryKeyTable.Delete(gCtx.KVStore(store), &m)

		require.NoError(t, err)
		t.Logf("%d: gas consumed on delete: %d", i, gCtx.GasConsumed())
	}
}

func TestExportImportStateAutoUInt64Table(t *testing.T) {
	interfaceRegistry := types.NewInterfaceRegistry()
	cdc := codec.NewProtoCodec(interfaceRegistry)

	ctx := NewMockContext()
	store := ctx.KVStore(storetypes.NewKVStoreKey("test"))

	k := NewTestKeeper(cdc)

	testRecordsNum := 10
	for i := 1; i <= testRecordsNum; i++ {
		tm := testdata.TableModel{
			Id:       uint64(i),
			Name:     fmt.Sprintf("my test %d", i),
			Metadata: bytes.Repeat([]byte{byte(i)}, metadataLen),
		}

		rowID, err := k.autoUInt64Table.Create(store, &tm)
		require.NoError(t, err)
		require.Equal(t, uint64(i), rowID)
	}
	var tms []*testdata.TableModel
	seqVal, err := k.autoUInt64Table.Export(store, &tms)
	require.NoError(t, err)
	require.Equal(t, seqVal, uint64(testRecordsNum))

	// when a new db seeded
	ctx = NewMockContext()
	store = ctx.KVStore(storetypes.NewKVStoreKey("test"))

	err = k.autoUInt64Table.Import(store, tms, seqVal)
	require.NoError(t, err)

	// then all data is set again
	for i := 1; i <= testRecordsNum; i++ {
		require.True(t, k.autoUInt64Table.Has(store, uint64(i)))
		var loaded testdata.TableModel
		rowID, err := k.autoUInt64Table.GetOne(store, uint64(i), &loaded)
		require.NoError(t, err)

		require.Equal(t, RowID(EncodeSequence(uint64(i))), rowID)
		assert.Equal(t, fmt.Sprintf("my test %d", i), loaded.Name)
		exp := bytes.Repeat([]byte{byte(i)}, metadataLen)
		assert.Equal(t, exp, loaded.Metadata)

		// and also the indexes
		exists, err := k.autoUInt64TableModelByMetadataIndex.Has(store, exp)
		require.NoError(t, err)
		require.True(t, exists)

		it, err := k.autoUInt64TableModelByMetadataIndex.Get(store, exp)
		require.NoError(t, err)
		var all []testdata.TableModel
		_, err = ReadAll(it, &all)
		require.NoError(t, err)
		require.Len(t, all, 1)
		assert.Equal(t, loaded, all[0])
	}
	require.Equal(t, uint64(testRecordsNum), k.autoUInt64Table.Sequence().CurVal(store))
}

func TestExportImportStatePrimaryKeyTable(t *testing.T) {
	interfaceRegistry := types.NewInterfaceRegistry()
	cdc := codec.NewProtoCodec(interfaceRegistry)

	ctx := NewMockContext()
	store := ctx.KVStore(storetypes.NewKVStoreKey("test"))

	k := NewTestKeeper(cdc)

	testRecordsNum := 10
	testRecords := make([]testdata.TableModel, testRecordsNum)
	for i := 1; i <= testRecordsNum; i++ {
		tm := testdata.TableModel{
			Id:       uint64(i),
			Name:     fmt.Sprintf("my test %d", i),
			Number:   uint64(i - 1),
			Metadata: bytes.Repeat([]byte{byte(i)}, metadataLen),
		}

		err := k.primaryKeyTable.Create(store, &tm)
		require.NoError(t, err)
		testRecords[i-1] = tm
	}
	var tms []*testdata.TableModel
	_, err := k.primaryKeyTable.Export(store, &tms)
	require.NoError(t, err)

	// when a new db seeded
	ctx = NewMockContext()
	store = ctx.KVStore(storetypes.NewKVStoreKey("test"))

	err = k.primaryKeyTable.Import(store, tms, 0)
	require.NoError(t, err)

	// then all data is set again
	it, err := k.primaryKeyTable.PrefixScan(store, nil, nil)
	require.NoError(t, err)
	var loaded []testdata.TableModel
	keys, err := ReadAll(it, &loaded)
	require.NoError(t, err)
	for i := range keys {
		assert.Equal(t, PrimaryKey(&testRecords[i]), keys[i].Bytes())
	}
	assert.Equal(t, testRecords, loaded)

	// all indexes setup
	for _, v := range testRecords {
		assertIndex(t, store, k.primaryKeyTableModelByNameIndex, v, v.Name)
		assertIndex(t, store, k.primaryKeyTableModelByNumberIndex, v, v.Number)
		assertIndex(t, store, k.primaryKeyTableModelByMetadataIndex, v, v.Metadata)
	}
}

func assertIndex(t *testing.T, store storetypes.KVStore, index Index, v testdata.TableModel, searchKey any) {
	t.Helper()

	it, err := index.Get(store, searchKey)
	require.NoError(t, err)

	var loaded []testdata.TableModel
	keys, err := ReadAll(it, &loaded)
	require.NoError(t, err)
	assert.Equal(t, []RowID{PrimaryKey(&v)}, keys)
	assert.Equal(t, []testdata.TableModel{v}, loaded)
}

func first(t *testing.T, it Iterator) ([]byte, testdata.TableModel) {
	t.Helper()

	var loaded testdata.TableModel
	key, err := First(it, &loaded)
	require.NoError(t, err)
	return key, loaded
}
