package keeper_test

import (
	"errors"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"cosmossdk.io/collections"

	"git.cw.tr/mukan-network/mukan-sdk/testutil/testdata"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	"git.cw.tr/mukan-network/mukan-sdk/x/gov/types"
	v1 "git.cw.tr/mukan-network/mukan-sdk/x/gov/types/v1"
	"git.cw.tr/mukan-network/mukan-sdk/x/gov/types/v1beta1"
)

// TODO(tip): remove this
func (suite *KeeperTestSuite) TestGetSetProposal() {
	testCases := map[string]struct {
		expedited bool
	}{
		"regular proposal": {},
		"expedited proposal": {
			expedited: true,
		},
	}

	for _, tc := range testCases {
		tp := TestProposal
		proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", suite.addrs[0], tc.expedited)
		suite.Require().NoError(err)
		proposalID := proposal.Id
		suite.Require().NoError(suite.govKeeper.SetProposal(suite.ctx, proposal))

		gotProposal, err := suite.govKeeper.Proposals.Get(suite.ctx, proposalID)
		suite.Require().Nil(err)
		suite.Require().Equal(proposal, gotProposal)
	}
}

// TODO(tip): remove this
func (suite *KeeperTestSuite) TestDeleteProposal() {
	testCases := map[string]struct {
		expedited bool
	}{
		"regular proposal": {},
		"expedited proposal": {
			expedited: true,
		},
	}

	for _, tc := range testCases {
		// delete non-existing proposal
		suite.Require().ErrorIs(suite.govKeeper.DeleteProposal(suite.ctx, 10), collections.ErrNotFound)

		tp := TestProposal
		proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", suite.addrs[0], tc.expedited)
		suite.Require().NoError(err)
		proposalID := proposal.Id
		suite.Require().NoError(suite.govKeeper.SetProposal(suite.ctx, proposal))
		suite.Require().NotPanics(func() {
			suite.Require().NoError(suite.govKeeper.DeleteProposal(suite.ctx, proposalID))
		}, "")
	}
}

func (suite *KeeperTestSuite) TestActivateVotingPeriod() {
	testCases := []struct {
		name      string
		expedited bool
	}{
		{name: "regular proposal"},
		{name: "expedited proposal", expedited: true},
	}

	for _, tc := range testCases {
		tp := TestProposal
		proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", suite.addrs[0], tc.expedited)
		suite.Require().NoError(err)

		suite.Require().Nil(proposal.VotingStartTime)

		suite.Require().NoError(suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal))

		proposal, err = suite.govKeeper.Proposals.Get(suite.ctx, proposal.Id)
		suite.Require().Nil(err)
		suite.Require().True(proposal.VotingStartTime.Equal(suite.ctx.BlockHeader().Time))

		has, err := suite.govKeeper.ActiveProposalsQueue.Has(suite.ctx, collections.Join(*proposal.VotingEndTime, proposal.Id))
		suite.Require().NoError(err)
		suite.Require().True(has)
		suite.Require().NoError(suite.govKeeper.DeleteProposal(suite.ctx, proposal.Id))
	}
}

func (suite *KeeperTestSuite) TestDeleteProposalInVotingPeriod() {
	testCases := []struct {
		name      string
		expedited bool
	}{
		{name: "regular proposal"},
		{name: "expedited proposal", expedited: true},
	}

	for _, tc := range testCases {
		suite.reset()
		tp := TestProposal
		proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", suite.addrs[0], tc.expedited)
		suite.Require().NoError(err)
		suite.Require().Nil(proposal.VotingStartTime)

		suite.Require().NoError(suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal))

		proposal, err = suite.govKeeper.Proposals.Get(suite.ctx, proposal.Id)
		suite.Require().Nil(err)
		suite.Require().True(proposal.VotingStartTime.Equal(suite.ctx.BlockHeader().Time))

		has, err := suite.govKeeper.ActiveProposalsQueue.Has(suite.ctx, collections.Join(*proposal.VotingEndTime, proposal.Id))
		suite.Require().NoError(err)
		suite.Require().True(has)

		// add vote
		voteOptions := []*v1.WeightedVoteOption{{Option: v1.OptionYes, Weight: "1.0"}}
		err = suite.govKeeper.AddVote(suite.ctx, proposal.Id, suite.addrs[0], voteOptions, "")
		suite.Require().NoError(err)

		suite.Require().NoError(suite.govKeeper.DeleteProposal(suite.ctx, proposal.Id))

		// add vote but proposal is deleted along with its VotingPeriodProposalKey
		err = suite.govKeeper.AddVote(suite.ctx, proposal.Id, suite.addrs[0], voteOptions, "")
		suite.Require().ErrorContains(err, ": inactive proposal")
	}
}

type invalidProposalRoute struct{ v1beta1.TextProposal }

func (invalidProposalRoute) ProposalRoute() string { return "nonexistingroute" }

func (suite *KeeperTestSuite) TestSubmitProposal() {
	govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress().String()
	_, _, randomAddr := testdata.KeyTestPubAddr()
	tp := v1beta1.TextProposal{Title: "title", Description: "description"}

	testCases := []struct {
		content     v1beta1.Content
		authority   string
		metadata    string
		expedited   bool
		expectedErr error
	}{
		{&tp, govAcct, "", false, nil},
		{&tp, govAcct, "", true, nil},
		// Keeper does not check the validity of title and description, no error
		{&v1beta1.TextProposal{Title: "", Description: "description"}, govAcct, "", false, nil},
		{&v1beta1.TextProposal{Title: strings.Repeat("1234567890", 100), Description: "description"}, govAcct, "", false, nil},
		{&v1beta1.TextProposal{Title: "title", Description: ""}, govAcct, "", false, nil},
		{&v1beta1.TextProposal{Title: "title", Description: strings.Repeat("1234567890", 1000)}, govAcct, "", true, nil},
		// error when metadata is too long (>10000)
		{&tp, govAcct, strings.Repeat("a", 100001), true, types.ErrMetadataTooLong},
		// error when signer is not gov acct
		{&tp, randomAddr.String(), "", false, types.ErrInvalidSigner},
		// error only when invalid route
		{&invalidProposalRoute{}, govAcct, "", false, types.ErrNoProposalHandlerExists},
	}

	for i, tc := range testCases {
		prop, err := v1.NewLegacyContent(tc.content, tc.authority)
		suite.Require().NoError(err)
		_, err = suite.govKeeper.SubmitProposal(suite.ctx, []sdk.Msg{prop}, tc.metadata, "title", "", suite.addrs[0], tc.expedited)
		suite.Require().True(errors.Is(tc.expectedErr, err), "tc #%d; got: %v, expected: %v", i, err, tc.expectedErr)
	}
}

func (suite *KeeperTestSuite) TestCancelProposal() {
	govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress().String()
	tp := v1beta1.TextProposal{Title: "title", Description: "description"}
	prop, err := v1.NewLegacyContent(&tp, govAcct)
	suite.Require().NoError(err)
	proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, []sdk.Msg{prop}, "", "title", "summary", suite.addrs[0], false)
	suite.Require().NoError(err)
	proposalID := proposal.Id

	proposal2, err := suite.govKeeper.SubmitProposal(suite.ctx, []sdk.Msg{prop}, "", "title", "summary", suite.addrs[1], true)
	suite.Require().NoError(err)
	proposal2ID := proposal2.Id

	// proposal3 is only used to check the votes for proposals which doesn't go through `CancelProposal` are still present in state
	proposal3, err := suite.govKeeper.SubmitProposal(suite.ctx, []sdk.Msg{prop}, "", "title", "summary", suite.addrs[2], false)
	suite.Require().NoError(err)
	proposal3ID := proposal3.Id

	// add votes for proposal 3
	suite.Require().NoError(suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal3))

	proposal3, err = suite.govKeeper.Proposals.Get(suite.ctx, proposal3ID)
	suite.Require().Nil(err)
	suite.Require().True(proposal3.VotingStartTime.Equal(suite.ctx.BlockHeader().Time))
	// add vote
	voteOptions := []*v1.WeightedVoteOption{{Option: v1.OptionYes, Weight: "1.0"}}
	err = suite.govKeeper.AddVote(suite.ctx, proposal3ID, suite.addrs[0], voteOptions, "")
	suite.Require().NoError(err)

	testCases := []struct {
		name        string
		malleate    func() (proposalID uint64, proposer string)
		proposalID  uint64
		proposer    string
		expectedErr bool
	}{
		{
			name: "without proposer",
			malleate: func() (uint64, string) {
				return 1, ""
			},
			expectedErr: true,
		},
		{
			name: "invalid proposal id",
			malleate: func() (uint64, string) {
				return 1, suite.addrs[1].String()
			},
			expectedErr: true,
		},
		{
			name: "valid proposalID but invalid proposer",
			malleate: func() (uint64, string) {
				return proposalID, suite.addrs[1].String()
			},
			expectedErr: true,
		},
		{
			name: "valid proposalID but invalid proposal which has already passed",
			malleate: func() (uint64, string) {
				// making proposal status pass
				proposal2, err := suite.govKeeper.Proposals.Get(suite.ctx, proposal2ID)
				suite.Require().Nil(err)

				proposal2.Status = v1.ProposalStatus_PROPOSAL_STATUS_PASSED
				suite.Require().NoError(suite.govKeeper.SetProposal(suite.ctx, proposal2))

				return proposal2ID, suite.addrs[1].String()
			},
			expectedErr: true,
		},
		{
			name: "valid proposer and proposal id",
			malleate: func() (uint64, string) {
				return proposalID, suite.addrs[0].String()
			},
			expectedErr: false,
		},
		{
			name: "valid case with deletion of votes",
			malleate: func() (uint64, string) {
				suite.Require().NoError(suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal))

				proposal, err = suite.govKeeper.Proposals.Get(suite.ctx, proposal.Id)
				suite.Require().Nil(err)
				suite.Require().True(proposal.VotingStartTime.Equal(suite.ctx.BlockHeader().Time))

				// add vote
				voteOptions := []*v1.WeightedVoteOption{{Option: v1.OptionYes, Weight: "1.0"}}
				err = suite.govKeeper.AddVote(suite.ctx, proposalID, suite.addrs[0], voteOptions, "")
				suite.Require().NoError(err)
				vote, err := suite.govKeeper.Votes.Get(suite.ctx, collections.Join(proposalID, suite.addrs[0]))
				suite.Require().NoError(err)
				suite.Require().NotNil(vote)

				return proposalID, suite.addrs[0].String()
			},
			expectedErr: false,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			pID, proposer := tc.malleate()
			err = suite.govKeeper.CancelProposal(suite.ctx, pID, proposer)
			if tc.expectedErr {
				suite.Require().Error(err)
			} else {
				suite.Require().NoError(err)
			}
		})
	}
	_, err = suite.govKeeper.Votes.Get(suite.ctx, collections.Join(proposalID, suite.addrs[0]))
	suite.Require().ErrorContains(err, collections.ErrNotFound.Error())

	// check that proposal 3 votes are still present in the state
	votes, err := suite.govKeeper.Votes.Get(suite.ctx, collections.Join(proposal3ID, suite.addrs[0]))
	suite.Require().NoError(err)
	suite.Require().NotNil(votes)
}

func TestMigrateProposalMessages(t *testing.T) {
	content := v1beta1.NewTextProposal("Test", "description")
	contentMsg, err := v1.NewLegacyContent(content, sdk.AccAddress("test1").String())
	require.NoError(t, err)
	content, err = v1.LegacyContentFromMessage(contentMsg)
	require.NoError(t, err)
	require.Equal(t, "Test", content.GetTitle())
	require.Equal(t, "description", content.GetDescription())
}
