package simsx

import (
	"context"
	"errors"
	"math/rand"
	"testing"
	"time"

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

	"cosmossdk.io/log"

	"git.cw.tr/mukan-network/mukan-sdk/testutil/testdata"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	simtypes "git.cw.tr/mukan-network/mukan-sdk/types/simulation"
)

func TestSimsMsgRegistryAdapter(t *testing.T) {
	senderAcc := SimAccountFixture()
	accs := []simtypes.Account{senderAcc.Account}
	ak := MockAccountSourceX{GetAccountFn: MemoryAccountSource(senderAcc).GetAccount}
	myMsg := testdata.NewTestMsg(senderAcc.Address)
	ctx := sdk.Context{}.WithContext(context.Background())
	futureTime := time.Now().Add(time.Second)

	specs := map[string]struct {
		factory           SimMsgFactoryX
		expFactoryMsg     sdk.Msg
		expFactoryErr     error
		expDeliveryErr    error
		expFutureOpsCount int
	}{
		"successful execution": {
			factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
				return []SimAccount{senderAcc}, myMsg
			}),
			expFactoryMsg: myMsg,
		},
		"skip execution": {
			factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
				reporter.Skip("testing")
				return nil, nil
			}),
		},
		"future ops registration": {
			factory: NewSimMsgFactoryWithFutureOps[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) (signer []SimAccount, msg *testdata.TestMsg) {
				fOpsReg.Add(futureTime, SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
					return []SimAccount{senderAcc}, myMsg
				}))
				return []SimAccount{senderAcc}, myMsg
			}),
			expFactoryMsg:     myMsg,
			expFutureOpsCount: 1,
		},
		"error in factory execution": {
			factory: NewSimMsgFactoryWithFutureOps[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter, fOpsReg FutureOpsRegistry) (signer []SimAccount, msg *testdata.TestMsg) {
				reporter.Fail(errors.New("testing"))
				return nil, nil
			}),
			expFactoryErr: errors.New("testing"),
		},
		"missing senders": {
			factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
				return nil, myMsg
			}),
			expDeliveryErr: errors.New("no senders"),
		},
		"error in delivery execution": {
			factory: SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
				return []SimAccount{senderAcc}, myMsg
			}),
			expDeliveryErr: errors.New("testing"),
		},
	}
	for name, spec := range specs {
		t.Run(name, func(t *testing.T) {
			r := NewBasicSimulationReporter()
			reg := NewSimsMsgRegistryAdapter(r, ak, nil, txConfig(), log.NewNopLogger())
			// when
			reg.Add(100, spec.factory)
			// then
			gotOps := reg.items
			require.Len(t, gotOps, 1)
			assert.Equal(t, uint32(100), gotOps[0].weight)

			// and when ops executed
			var capturedTXs []sdk.Tx
			captureTXApp := AppEntrypointFn(func(_txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
				capturedTXs = append(capturedTXs, tx)
				return sdk.GasInfo{}, &sdk.Result{}, spec.expDeliveryErr
			})
			fn := gotOps[0].op
			gotOpsResult, gotFOps, gotErr := fn(rand.New(rand.NewSource(1)), captureTXApp, ctx, accs, "testchain")
			// then
			if spec.expFactoryErr != nil {
				require.Equal(t, spec.expFactoryErr, gotErr)
				assert.Empty(t, gotFOps)
				assert.Equal(t, gotOpsResult.OK, spec.expFactoryErr == nil)
				assert.Empty(t, gotOpsResult.Comment)
				require.Empty(t, capturedTXs)
			}
			if spec.expDeliveryErr != nil {
				require.Equal(t, spec.expDeliveryErr, gotErr)
			}
			// and verify TX delivery
			if spec.expFactoryMsg != nil {
				require.Len(t, capturedTXs, 1)
				require.Len(t, capturedTXs[0].GetMsgs(), 1)
				assert.Equal(t, spec.expFactoryMsg, capturedTXs[0].GetMsgs()[0])
			}
			assert.Len(t, gotFOps, spec.expFutureOpsCount)
		})
	}
}

func TestUniqueTypeRegistry(t *testing.T) {
	exampleFactory := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
		return []SimAccount{}, nil
	})
	exampleFactory2 := SimMsgFactoryFn[*testdata.MsgCreateDog](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.MsgCreateDog) {
		return []SimAccount{}, nil
	})

	specs := map[string]struct {
		src    []WeightedFactory
		exp    []WeightedFactoryMethod
		expErr bool
	}{
		"unique": {
			src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}},
			exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}},
		},
		"sorted": {
			src: []WeightedFactory{{Weight: 2, Factory: exampleFactory2}, {Weight: 1, Factory: exampleFactory}},
			exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}, {Weight: 2, Factory: exampleFactory2.Create()}},
		},
		"duplicate": {
			src:    []WeightedFactory{{Weight: 1, Factory: exampleFactory}, {Weight: 2, Factory: exampleFactory}},
			expErr: true,
		},
	}
	for name, spec := range specs {
		t.Run(name, func(t *testing.T) {
			reg := NewUniqueTypeRegistry()
			if spec.expErr {
				require.Panics(t, func() {
					for _, v := range spec.src {
						reg.Add(v.Weight, v.Factory)
					}
				})
				return
			}
			for _, v := range spec.src {
				reg.Add(v.Weight, v.Factory)
			}
			// then
			got := readAll(reg.Iterator())
			require.Len(t, got, len(spec.exp))
		})
	}
}

func TestWeightedFactories(t *testing.T) {
	r := NewWeightedFactoryMethods()
	f1 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
		panic("not implemented")
	}
	f2 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
		panic("not implemented")
	}
	r.Add(1, f1)
	r.Add(2, f2)
	got := readAll(r.Iterator())
	require.Len(t, got, 2)

	assert.Equal(t, uint32(1), r[0].Weight)
	assert.Equal(t, uint32(2), r[1].Weight)
}

func TestAppendIterators(t *testing.T) {
	r1 := NewWeightedFactoryMethods()
	r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
		panic("not implemented")
	})
	r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
		panic("not implemented")
	})
	r1.Add(3, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
		panic("not implemented")
	})
	r2 := NewUniqueTypeRegistry()
	r2.Add(1, SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
		panic("not implemented")
	}))
	// when
	all := readAll(AppendIterators(r1.Iterator(), r2.Iterator()))
	// then
	require.Len(t, all, 4)
	gotWeights := Collect(all, func(a WeightedFactoryMethod) uint32 { return a.Weight })
	assert.Equal(t, []uint32{2, 2, 3, 1}, gotWeights)
}

func readAll(iterator WeightedProposalMsgIter) []WeightedFactoryMethod {
	var ret []WeightedFactoryMethod
	for w, f := range iterator {
		ret = append(ret, WeightedFactoryMethod{Weight: w, Factory: f})
	}
	return ret
}
