package client_test

import (
	"strings"
	"testing"

	testifysuite "github.com/stretchr/testify/suite"

	upgradetypes "cosmossdk.io/x/upgrade/types"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	cmtproto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/types"

	client "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	ibctm "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/07-tendermint"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)

type ClientTestSuite struct {
	testifysuite.Suite

	coordinator *ibctesting.Coordinator

	chainA *ibctesting.TestChain
	chainB *ibctesting.TestChain
}

func (suite *ClientTestSuite) SetupTest() {
	suite.coordinator = ibctesting.NewCoordinator(suite.T(), 2)

	suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1))
	suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2))
}

func TestClientTestSuite(t *testing.T) {
	testifysuite.Run(t, new(ClientTestSuite))
}

func (suite *ClientTestSuite) TestBeginBlocker() {
	for range 10 {
		// increment height
		suite.coordinator.CommitBlock(suite.chainA, suite.chainB)

		suite.Require().NotPanics(func() {
			client.BeginBlocker(suite.chainA.GetContext(), suite.chainA.App.GetIBCKeeper().ClientKeeper)
		}, "BeginBlocker shouldn't panic")
	}
}

func (suite *ClientTestSuite) TestBeginBlockerConsensusState() {
	plan := &upgradetypes.Plan{
		Name:   "test",
		Height: suite.chainA.GetContext().BlockHeight() + 1,
	}
	// set upgrade plan in the upgrade store
	store := suite.chainA.GetContext().KVStore(suite.chainA.GetSimApp().GetKey(upgradetypes.StoreKey))
	bz := suite.chainA.App.AppCodec().MustMarshal(plan)
	store.Set(upgradetypes.PlanKey(), bz)

	nextValsHash := []byte("nextValsHash")
	newCtx := suite.chainA.GetContext().WithBlockHeader(cmtproto.Header{
		ChainID:            suite.chainA.ChainID,
		Height:             suite.chainA.GetContext().BlockHeight(),
		NextValidatorsHash: nextValsHash,
	})

	err := suite.chainA.GetSimApp().UpgradeKeeper.SetUpgradedClient(newCtx, plan.Height, []byte("client state"))
	suite.Require().NoError(err)

	client.BeginBlocker(newCtx, suite.chainA.App.GetIBCKeeper().ClientKeeper)

	// plan Height is at ctx.BlockHeight+1
	consState, err := suite.chainA.GetSimApp().UpgradeKeeper.GetUpgradedConsensusState(newCtx, plan.Height)
	suite.Require().NoError(err)

	bz, err = types.MarshalConsensusState(suite.chainA.App.AppCodec(), &ibctm.ConsensusState{Timestamp: newCtx.BlockTime(), NextValidatorsHash: nextValsHash})
	suite.Require().NoError(err)
	suite.Require().Equal(bz, consState)
}

func (suite *ClientTestSuite) TestBeginBlockerUpgradeEvents() {
	plan := &upgradetypes.Plan{
		Name:   "test",
		Height: suite.chainA.GetContext().BlockHeight() + 1,
	}
	// set upgrade plan in the upgrade store
	store := suite.chainA.GetContext().KVStore(suite.chainA.GetSimApp().GetKey(upgradetypes.StoreKey))
	bz := suite.chainA.App.AppCodec().MustMarshal(plan)
	store.Set(upgradetypes.PlanKey(), bz)

	nextValsHash := []byte("nextValsHash")
	newCtx := suite.chainA.GetContext().WithBlockHeader(cmtproto.Header{
		Height:             suite.chainA.GetContext().BlockHeight(),
		NextValidatorsHash: nextValsHash,
	})

	err := suite.chainA.GetSimApp().UpgradeKeeper.SetUpgradedClient(newCtx, plan.Height, []byte("client state"))
	suite.Require().NoError(err)

	cacheCtx, writeCache := suite.chainA.GetContext().CacheContext()

	client.BeginBlocker(cacheCtx, suite.chainA.App.GetIBCKeeper().ClientKeeper)
	writeCache()

	suite.requireContainsEvent(cacheCtx.EventManager().Events(), types.EventTypeUpgradeChain, true)
}

func (suite *ClientTestSuite) TestBeginBlockerUpgradeEventsAbsence() {
	cacheCtx, writeCache := suite.chainA.GetContext().CacheContext()
	client.BeginBlocker(suite.chainA.GetContext(), suite.chainA.App.GetIBCKeeper().ClientKeeper)
	writeCache()
	suite.requireContainsEvent(cacheCtx.EventManager().Events(), types.EventTypeUpgradeChain, false)
}

// requireContainsEvent verifies if an event of a specific type was emitted.
func (suite *ClientTestSuite) requireContainsEvent(events sdk.Events, eventType string, shouldContain bool) {
	found := false
	var eventTypes []string
	for _, e := range events {
		eventTypes = append(eventTypes, e.Type)
		if e.Type == eventType {
			found = true
			break
		}
	}
	if shouldContain {
		suite.Require().True(found, "event type %s was not found in %s", eventType, strings.Join(eventTypes, ","))
	} else {
		suite.Require().False(found, "event type %s was found in %s", eventType, strings.Join(eventTypes, ","))
	}
}
