package simsx

import (
	"encoding/json"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"

	dbm "github.com/cosmos/cosmos-db"
	"github.com/stretchr/testify/require"

	"cosmossdk.io/log"

	"git.cw.tr/mukan-network/mukan-sdk/baseapp"
	"git.cw.tr/mukan-network/mukan-sdk/client"
	"git.cw.tr/mukan-network/mukan-sdk/client/flags"
	"git.cw.tr/mukan-network/mukan-sdk/codec"
	"git.cw.tr/mukan-network/mukan-sdk/runtime"
	servertypes "git.cw.tr/mukan-network/mukan-sdk/server/types"
	simtestutil "git.cw.tr/mukan-network/mukan-sdk/testutil/sims"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	"git.cw.tr/mukan-network/mukan-sdk/types/module"
	simtypes "git.cw.tr/mukan-network/mukan-sdk/types/simulation"
	"git.cw.tr/mukan-network/mukan-sdk/x/simulation"
	"git.cw.tr/mukan-network/mukan-sdk/x/simulation/client/cli"
)

const SimAppChainID = "simulation-app"

// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32
var defaultSeeds = []int64{
	1, 2, 4, 7,
	32, 123, 124, 582, 1893, 2989,
	3012, 4728, 37827, 981928, 87821, 891823782,
	989182, 89182391, 11, 22, 44, 77, 99, 2020,
	3232, 123123, 124124, 582582, 18931893,
	29892989, 30123012, 47284728, 7601778, 8090485,
	977367484, 491163361, 424254581, 673398983,
}

// SimStateFactory is a factory type that provides a convenient way to create a simulation state for testing.
// It contains the following fields:
// - Codec: a codec used for serializing other objects
// - AppStateFn: a function that returns the app state JSON bytes and the genesis accounts
// - BlockedAddr: a map of blocked addresses
// - AccountSource: an interface for retrieving accounts
// - BalanceSource: an interface for retrieving balance-related information
type SimStateFactory struct {
	Codec         codec.Codec
	AppStateFn    simtypes.AppStateFn
	BlockedAddr   map[string]bool
	AccountSource AccountSourceX
	BalanceSource BalanceSource
}

// SimulationApp abstract app that is used by sims
type SimulationApp interface {
	runtime.AppI
	SetNotSigverifyTx()
	GetBaseApp() *baseapp.BaseApp
	TxConfig() client.TxConfig
	Close() error
}

// Run is a helper function that runs a simulation test with the given parameters.
// It calls the RunWithSeeds function with the default seeds and parameters.
//
// This is the entrypoint to run simulation tests that used to run with the runsim binary.
func Run[T SimulationApp](
	t *testing.T,
	appFactory func(
		logger log.Logger,
		db dbm.DB,
		traceStore io.Writer,
		loadLatest bool,
		appOpts servertypes.AppOptions,
		baseAppOptions ...func(*baseapp.BaseApp),
	) T,
	setupStateFactory func(app T) SimStateFactory,
	postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
	t.Helper()
	RunWithSeeds(t, appFactory, setupStateFactory, defaultSeeds, nil, postRunActions...)
}

// RunWithSeeds is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for each seed.
// The execution is deterministic and can be used for fuzz tests as well.
//
// The system under test is isolated for each run but unlike the old runsim command, there is no Process separation.
// This means, global caches may be reused for example. This implementation build upon the vanilla Go stdlib test framework.
func RunWithSeeds[T SimulationApp](
	t *testing.T,
	appFactory func(
		logger log.Logger,
		db dbm.DB,
		traceStore io.Writer,
		loadLatest bool,
		appOpts servertypes.AppOptions,
		baseAppOptions ...func(*baseapp.BaseApp),
	) T,
	setupStateFactory func(app T) SimStateFactory,
	seeds []int64,
	fuzzSeed []byte,
	postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
	t.Helper()
	RunWithSeedsAndRandAcc(t, appFactory, setupStateFactory, seeds, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}

// RunWithSeedsAndRandAcc calls RunWithSeeds with randAccFn
func RunWithSeedsAndRandAcc[T SimulationApp](
	t *testing.T,
	appFactory func(
		logger log.Logger,
		db dbm.DB,
		traceStore io.Writer,
		loadLatest bool,
		appOpts servertypes.AppOptions,
		baseAppOptions ...func(*baseapp.BaseApp),
	) T,
	setupStateFactory func(app T) SimStateFactory,
	seeds []int64,
	fuzzSeed []byte,
	randAccFn simtypes.RandomAccountFn,
	postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
	t.Helper()
	if deprecatedParams := cli.GetDeprecatedFlagUsed(); len(deprecatedParams) != 0 {
		fmt.Printf("Warning: Deprecated flag are used: %s", strings.Join(deprecatedParams, ","))
	}

	cfg := cli.NewConfigFromFlags()
	cfg.ChainID = SimAppChainID
	for i := range seeds {
		seed := seeds[i]
		t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) {
			t.Parallel()
			RunWithSeed(t, cfg, appFactory, setupStateFactory, seed, fuzzSeed, postRunActions...)
		})
	}
}

// RunWithSeed is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for the seed.
// The execution is deterministic and can be used for fuzz tests as well.
func RunWithSeed[T SimulationApp](
	tb testing.TB,
	cfg simtypes.Config,
	appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
	setupStateFactory func(app T) SimStateFactory,
	seed int64,
	fuzzSeed []byte,
	postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
	tb.Helper()
	RunWithSeedAndRandAcc(tb, cfg, appFactory, setupStateFactory, seed, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}

// RunWithSeedAndRandAcc calls RunWithSeed with randAccFn
func RunWithSeedAndRandAcc[T SimulationApp](
	tb testing.TB,
	cfg simtypes.Config,
	appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
	setupStateFactory func(app T) SimStateFactory,
	seed int64,
	fuzzSeed []byte,
	randAccFn simtypes.RandomAccountFn,
	postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
	tb.Helper()
	// setup environment
	tCfg := cfg.With(tb, seed, fuzzSeed)
	testInstance := NewSimulationAppInstance(tb, tCfg, appFactory)
	var runLogger log.Logger
	if cli.FlagVerboseValue {
		runLogger = log.NewTestLogger(tb)
	} else {
		runLogger = log.NewTestLoggerInfo(tb)
	}
	runLogger = runLogger.With("seed", tCfg.Seed)

	app := testInstance.App
	stateFactory := setupStateFactory(app)
	ops, reporter := prepareWeightedOps(app.SimulationManager(), stateFactory, tCfg, testInstance.App.TxConfig(), runLogger)
	simParams, accs, err := simulation.SimulateFromSeedX(
		tb,
		runLogger,
		WriteToDebugLog(runLogger),
		app.GetBaseApp(),
		stateFactory.AppStateFn,
		randAccFn,
		ops,
		stateFactory.BlockedAddr,
		tCfg,
		stateFactory.Codec,
		testInstance.ExecLogWriter,
	)
	require.NoError(tb, err)
	err = simtestutil.CheckExportSimulation(app, tCfg, simParams)
	require.NoError(tb, err)
	if tCfg.Commit {
		simtestutil.PrintStats(testInstance.DB)
	}
	// not using tb.Log to always print the summary
	fmt.Printf("+++ DONE (seed: %d): \n%s\n", seed, reporter.Summary().String())
	for _, step := range postRunActions {
		step(tb, testInstance, accs)
	}
	require.NoError(tb, app.Close())
}

type (
	HasWeightedOperationsX interface {
		WeightedOperationsX(weight WeightSource, reg Registry)
	}
	HasWeightedOperationsXWithProposals interface {
		WeightedOperationsX(weights WeightSource, reg Registry, proposals WeightedProposalMsgIter,
			legacyProposals []simtypes.WeightedProposalContent) //nolint: staticcheck // used for legacy proposal types
	}
	HasProposalMsgsX interface {
		ProposalMsgsX(weights WeightSource, reg Registry)
	}
)

type (
	HasLegacyWeightedOperations interface {
		// WeightedOperations simulation operations (i.e msgs) with their respective weight
		WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation
	}
	// HasLegacyProposalMsgs defines the messages that can be used to simulate governance (v1) proposals
	// Deprecated replaced by HasProposalMsgsX
	HasLegacyProposalMsgs interface {
		// ProposalMsgs msg fu	nctions used to simulate governance proposals
		ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg
	}

	// HasLegacyProposalContents defines the contents that can be used to simulate legacy governance (v1beta1) proposals
	// Deprecated replaced by HasProposalMsgsX
	HasLegacyProposalContents interface {
		// ProposalContents content functions used to simulate governance proposals
		ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent //nolint:staticcheck // legacy v1beta1 governance
	}
)

// TestInstance is a generic type that represents an instance of a SimulationApp used for testing simulations.
// It contains the following fields:
//   - App: The instance of the SimulationApp under test.
//   - DB: The LevelDB database for the simulation app.
//   - WorkDir: The temporary working directory for the simulation app.
//   - Cfg: The configuration flags for the simulator.
//   - AppLogger: The logger used for logging in the app during the simulation, with seed value attached.
//   - ExecLogWriter: Captures block and operation data coming from the simulation
type TestInstance[T SimulationApp] struct {
	App           T
	DB            dbm.DB
	WorkDir       string
	Cfg           simtypes.Config
	AppLogger     log.Logger
	ExecLogWriter simulation.LogWriter
}

// included to avoid cyclic dependency in testutils/sims
func prepareWeightedOps(
	sm *module.SimulationManager,
	stateFact SimStateFactory,
	config simtypes.Config,
	txConfig client.TxConfig,
	logger log.Logger,
) (simulation.WeightedOperations, *BasicSimulationReporter) {
	cdc := stateFact.Codec
	simState := module.SimulationState{
		AppParams: make(simtypes.AppParams),
		Cdc:       cdc,
		TxConfig:  txConfig,
		BondDenom: sdk.DefaultBondDenom,
	}

	if config.ParamsFile != "" {
		bz, err := os.ReadFile(config.ParamsFile)
		if err != nil {
			panic(err)
		}

		err = json.Unmarshal(bz, &simState.AppParams)
		if err != nil {
			panic(err)
		}
	}

	weights := ParamWeightSource(simState.AppParams)
	reporter := NewBasicSimulationReporter()

	pReg := make(UniqueTypeRegistry)
	wContent := make([]simtypes.WeightedProposalContent, 0) //nolint:staticcheck // required for legacy type
	legacyPReg := NewWeightedFactoryMethods()
	// add gov proposals types
	for _, m := range sm.Modules {
		switch xm := m.(type) {
		case HasProposalMsgsX:
			xm.ProposalMsgsX(weights, pReg)
		case HasLegacyProposalMsgs:
			for _, p := range xm.ProposalMsgs(simState) {
				weight := weights.Get(p.AppParamsKey(), safeUint(p.DefaultWeight()))
				legacyPReg.Add(weight, legacyToMsgFactoryAdapter(p.MsgSimulatorFn()))
			}
		case HasLegacyProposalContents:
			wContent = append(wContent, xm.ProposalContents(simState)...)
		}
	}

	oReg := NewSimsMsgRegistryAdapter(
		reporter,
		stateFact.AccountSource,
		stateFact.BalanceSource,
		txConfig,
		logger,
	)
	wOps := make([]simtypes.WeightedOperation, 0, len(sm.Modules))
	for _, m := range sm.Modules {
		// add operations
		switch xm := m.(type) {
		case HasWeightedOperationsX:
			xm.WeightedOperationsX(weights, oReg)
		case HasWeightedOperationsXWithProposals:
			xm.WeightedOperationsX(weights, oReg, AppendIterators(legacyPReg.Iterator(), pReg.Iterator()), wContent)
		case HasLegacyWeightedOperations:
			wOps = append(wOps, xm.WeightedOperations(simState)...)
		}
	}
	return append(wOps, Collect(oReg.items, func(a weightedOperation) simtypes.WeightedOperation { return a })...), reporter
}

func safeUint(p int) uint32 {
	if p < 0 || p > math.MaxUint32 {
		panic(fmt.Sprintf("can not cast to uint32: %d", p))
	}
	return uint32(p)
}

// NewSimulationAppInstance initializes and returns a TestInstance of a SimulationApp.
// The function takes a testing.T instance, a simtypes.Config instance, and an appFactory function as parameters.
// It creates a temporary working directory and a LevelDB database for the simulation app.
// The function then initializes a logger based on the verbosity flag and sets the logger's seed to the test configuration's seed.
// The database is closed and cleaned up on test completion.
func NewSimulationAppInstance[T SimulationApp](
	tb testing.TB,
	tCfg simtypes.Config,
	appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
) TestInstance[T] {
	tb.Helper()
	workDir := tb.TempDir()
	require.NoError(tb, os.Mkdir(filepath.Join(workDir, "data"), 0o750))
	dbDir := filepath.Join(workDir, "leveldb-app-sim")
	var logger log.Logger
	if cli.FlagVerboseValue {
		logger = log.NewTestLogger(tb)
	} else {
		logger = log.NewTestLoggerError(tb)
	}
	logger = logger.With("seed", tCfg.Seed)
	db, err := dbm.NewDB("Simulation", dbm.BackendType(tCfg.DBBackend), dbDir)
	require.NoError(tb, err)
	tb.Cleanup(func() {
		_ = db.Close() // ensure db is closed
	})
	appOptions := make(simtestutil.AppOptionsMap)
	appOptions[flags.FlagHome] = workDir
	opts := []func(*baseapp.BaseApp){baseapp.SetChainID(tCfg.ChainID)}
	if tCfg.FauxMerkle {
		opts = append(opts, FauxMerkleModeOpt)
	}
	app := appFactory(logger, db, nil, true, appOptions, opts...)
	if !cli.FlagSigverifyTxValue {
		app.SetNotSigverifyTx()
	}
	return TestInstance[T]{
		App:           app,
		DB:            db,
		WorkDir:       workDir,
		Cfg:           tCfg,
		AppLogger:     logger,
		ExecLogWriter: &simulation.StandardLogWriter{Seed: tCfg.Seed},
	}
}

var _ io.Writer = writerFn(nil)

type writerFn func(p []byte) (n int, err error)

func (w writerFn) Write(p []byte) (n int, err error) {
	return w(p)
}

// WriteToDebugLog is an adapter to io.Writer interface
func WriteToDebugLog(logger log.Logger) io.Writer {
	return writerFn(func(p []byte) (n int, err error) {
		logger.Debug(string(p))
		return len(p), nil
	})
}

// FauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of
// an IAVLStore for faster simulation speed.
func FauxMerkleModeOpt(bapp *baseapp.BaseApp) {
	bapp.SetFauxMerkleMode()
}
