package keeper_test

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	storetypes "cosmossdk.io/store/types"

	"git.cw.tr/mukan-network/mukan-sdk/runtime"
	"git.cw.tr/mukan-network/mukan-sdk/testutil"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	moduletestutil "git.cw.tr/mukan-network/mukan-sdk/types/module/testutil"
	authcodec "git.cw.tr/mukan-network/mukan-sdk/x/auth/codec"
	"git.cw.tr/mukan-network/mukan-sdk/x/auth/keeper"
	"git.cw.tr/mukan-network/mukan-sdk/x/auth/types"
)

func TestManager(t *testing.T) {
	var (
		mgr keeper.AccountKeeper
		ctx sdk.Context
	)
	encCfg := moduletestutil.MakeTestEncodingConfig()
	reset := func() {
		mockStoreKey := storetypes.NewKVStoreKey("test")
		storeService := runtime.NewKVStoreService(mockStoreKey)
		ctx = testutil.DefaultContextWithDB(t, mockStoreKey, storetypes.NewTransientStoreKey("transient_test")).Ctx
		mgr = keeper.NewAccountKeeper(
			encCfg.Codec,
			storeService,
			types.ProtoBaseAccount,
			nil,
			authcodec.NewBech32Codec("cosmos"),
			"cosmos",
			types.NewModuleAddress("gov").String(),
			keeper.WithUnorderedTransactions(true),
		)
	}

	type utxSequence struct {
		sender  []byte
		timeout time.Time
	}
	testCases := map[string]struct {
		addFunc           []utxSequence
		blockTime         time.Time
		expectContains    []utxSequence
		expectNotContains []utxSequence
	}{
		"transactions are not removed when block time is before every utx": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos3"),
					time.Unix(10, 0),
				},
			},
			blockTime: time.Unix(5, 0),
			expectContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos3"),
					time.Unix(10, 0),
				},
			},
		},
		"transactions are removed if their timeout is equal to the block time": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos3"),
					time.Unix(10, 0),
				},
			},
			blockTime: time.Unix(10, 10),
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos3"),
					time.Unix(10, 0),
				},
			},
		},
		"only some txs are removed": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(15, 0),
				},
				{
					[]byte("cosmos3"),
					time.Unix(20, 0),
				},
			},
			blockTime: time.Unix(16, 10),
			expectContains: []utxSequence{
				{
					[]byte("cosmos3"),
					time.Unix(20, 0),
				},
			},
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(15, 0),
				},
			},
		},
		"empty state - no transactions to remove": {
			addFunc:           []utxSequence{},
			blockTime:         time.Unix(10, 0),
			expectContains:    []utxSequence{},
			expectNotContains: []utxSequence{},
		},

		"multiple senders with same timestamp": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
			},
			blockTime: time.Unix(10, 1),
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 0),
				},
			},
		},

		"same sender with multiple timestamps": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos1"),
					time.Unix(15, 0),
				},
				{
					[]byte("cosmos1"),
					time.Unix(20, 0),
				},
			},
			blockTime: time.Unix(16, 0),
			expectContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(20, 0),
				},
			},
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 0),
				},
				{
					[]byte("cosmos1"),
					time.Unix(15, 0),
				},
			},
		},
		"nanosecond precision boundary test": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 999999998),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 999999999),
				},
				{
					[]byte("cosmos3"),
					time.Unix(11, 0),
				},
			},
			blockTime: time.Unix(10, 999999999),
			expectContains: []utxSequence{
				{
					[]byte("cosmos3"),
					time.Unix(11, 0),
				},
			},
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(10, 999999998),
				},
				{
					[]byte("cosmos2"),
					time.Unix(10, 999999999),
				},
			},
		},

		"zero timestamp test": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(0, 0),
				},
			},
			blockTime: time.Unix(1, 0),
			expectNotContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(0, 0),
				},
			},
		},
		"far future timestamp": {
			addFunc: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(2^30, 0), // Very far in the future
				},
			},
			blockTime: time.Unix(10, 0),
			expectContains: []utxSequence{
				{
					[]byte("cosmos1"),
					time.Unix(2^30, 0),
				},
			},
		},
	}

	for name, tc := range testCases {
		reset()
		t.Run(name, func(t *testing.T) {
			ctx = ctx.WithBlockTime(tc.blockTime)
			for _, seq := range tc.addFunc {
				err := mgr.TryAddUnorderedNonce(ctx, seq.sender, seq.timeout)
				t.Logf("added transaction: %d/%s", seq.timeout.UnixNano(), seq.sender)
				require.NoError(t, err)
			}
			t.Logf("removing txs. block_time: %d", tc.blockTime.UnixNano())
			err := mgr.RemoveExpiredUnorderedNonces(ctx)
			require.NoError(t, err)

			for _, seq := range tc.expectNotContains {
				has, err := mgr.ContainsUnorderedNonce(ctx, seq.sender, seq.timeout)
				require.NoError(t, err)
				require.False(t, has, "should not contain %s", seq.sender)
			}
			for _, seq := range tc.expectContains {
				has, err := mgr.ContainsUnorderedNonce(ctx, seq.sender, seq.timeout)
				require.NoError(t, err)
				require.True(t, has, "expected to contain %d/%s", uint64(seq.timeout.UnixNano()), seq.sender)
			}
		})
	}
}

func TestCannotAddDuplicate(t *testing.T) {
	mockStoreKey := storetypes.NewKVStoreKey("test")
	storeService := runtime.NewKVStoreService(mockStoreKey)
	ctx := testutil.DefaultContextWithDB(t, mockStoreKey, storetypes.NewTransientStoreKey("transient_test")).Ctx
	mgr := keeper.NewAccountKeeper(
		moduletestutil.MakeTestEncodingConfig().Codec,
		storeService,
		types.ProtoBaseAccount,
		nil,
		authcodec.NewBech32Codec("cosmos"),
		"cosmos",
		types.NewModuleAddress("gov").String(),
		keeper.WithUnorderedTransactions(true),
	)

	addUser := []byte("foo")
	timeout := time.Unix(10, 0)
	err := mgr.TryAddUnorderedNonce(ctx, addUser, timeout)
	require.NoError(t, err)

	err = mgr.TryAddUnorderedNonce(ctx, addUser, timeout)
	require.ErrorContains(t, err, "already used timeout")
}
