package queryserver_test

import (
	"strconv"

	cosmosMath "cosmossdk.io/math"

	alloraMath "github.com/allora-network/allora-chain/math"
	"github.com/allora-network/allora-chain/x/emissions/types"
)

func (s *QueryServerTestSuite) TestGetTotalStake() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()

	expectedTotalStake := cosmosMath.NewInt(1000)
	err := keeper.SetTotalStake(ctx, expectedTotalStake)
	s.Require().NoError(err, "SetTotalStake should not produce an error")

	req := &types.GetTotalStakeRequest{}
	response, err := queryServer.GetTotalStake(ctx, req)
	s.Require().NoError(err, "GetTotalStake should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(expectedTotalStake, response.Amount, "The retrieved total stake should match the expected value")
}

func (s *QueryServerTestSuite) TestGetReputerStakeInTopic() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	topicId := uint64(1)
	reputerAddr := s.AddrsStr(1)
	initialStake := cosmosMath.NewInt(250)

	err := keeper.AddReputerStake(ctx, topicId, reputerAddr, initialStake)
	s.Require().NoError(err, "AddStake should not produce an error")

	req := &types.GetReputerStakeInTopicRequest{
		Address: reputerAddr,
		TopicId: topicId,
	}

	response, err := queryServer.GetReputerStakeInTopic(ctx, req)
	s.Require().NoError(err, "GetReputerStakeInTopic should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(initialStake, response.Amount, "The retrieved stake should match the initial stake set for the reputer in the topic")
}

func (s *QueryServerTestSuite) TestGetMultiReputerStakeInTopic() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	topicId := uint64(1)
	reputer1Addr := s.AddrsStr(1)
	reputer2Addr := s.AddrsStr(2)
	initialStake1 := cosmosMath.NewInt(250)
	initialStake2 := cosmosMath.NewInt(251)

	err := keeper.AddReputerStake(ctx, topicId, reputer1Addr, initialStake1)
	s.Require().NoError(err, "AddStake should not produce an error")
	err = keeper.AddReputerStake(ctx, topicId, reputer2Addr, initialStake2)
	s.Require().NoError(err, "AddStake should not produce an error")

	req := &types.GetMultiReputerStakeInTopicRequest{
		Addresses: []string{reputer1Addr, reputer2Addr},
		TopicId:   topicId,
	}

	response, err := queryServer.GetMultiReputerStakeInTopic(ctx, req)
	s.Require().NoError(err, "GetReputerStakeInTopic should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Len(response.Amounts, 2, "The retrieved set of stakes should have length equal to the number of reputers queried")
	s.Require().Equal(initialStake1, response.Amounts[0].Amount, "The retrieved stake should match the initial stake set for the first reputer in the topic")
	s.Require().Equal(initialStake2, response.Amounts[1].Amount, "The retrieved stake should match the initial stake set for the second reputer in the topic")
}

func (s *QueryServerTestSuite) TestGetDelegateStakeInTopicInReputer() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	topicId := uint64(1)
	delegatorAddr := s.AddrsStr(0)
	reputerAddr := s.AddrsStr(1)
	initialStakeAmount := cosmosMath.NewInt(1000)

	err := keeper.AddDelegateStake(ctx, topicId, delegatorAddr, reputerAddr, initialStakeAmount)
	s.Require().NoError(err, "AddDelegateStake should not produce an error")

	req := &types.GetDelegateStakeInTopicInReputerRequest{
		ReputerAddress: reputerAddr,
		TopicId:        topicId,
	}

	response, err := queryServer.GetDelegateStakeInTopicInReputer(ctx, req)
	s.Require().NoError(err, "GetDelegateStakeInTopicInReputer should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(initialStakeAmount, response.Amount, "The retrieved delegate stake should match the initial stake set for the reputer in the topic")
}

func (s *QueryServerTestSuite) TestGetStakeFromDelegatorInTopicInReputer() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()

	topicId := uint64(1)
	delegatorAddr := s.AddrsStr(0)
	reputerAddr := s.AddrsStr(1)
	stakeAmount := cosmosMath.NewInt(50)

	err := keeper.AddDelegateStake(ctx, topicId, delegatorAddr, reputerAddr, stakeAmount)
	s.Require().NoError(err, "AddDelegateStake should not produce an error")

	req := &types.GetStakeFromDelegatorInTopicInReputerRequest{
		DelegatorAddress: delegatorAddr,
		ReputerAddress:   reputerAddr,
		TopicId:          topicId,
	}

	response, err := queryServer.GetStakeFromDelegatorInTopicInReputer(ctx, req)
	s.Require().NoError(err, "GetStakeFromDelegatorInTopicInReputer should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(stakeAmount, response.Amount, "The retrieved stake amount should match the delegated stake")
}

func (s *QueryServerTestSuite) TestGetStakeFromDelegatorInTopic() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()

	topicId := uint64(1)
	delegatorAddr := s.AddrsStr(0)
	initialStakeAmount := cosmosMath.NewInt(500)
	additionalStakeAmount := cosmosMath.NewInt(300)

	err := keeper.AddDelegateStake(ctx, topicId, delegatorAddr, s.AddrsStr(1), initialStakeAmount)
	s.Require().NoError(err)

	err = keeper.AddDelegateStake(ctx, topicId, delegatorAddr, s.AddrsStr(1), additionalStakeAmount)
	s.Require().NoError(err)

	req := &types.GetStakeFromDelegatorInTopicRequest{
		DelegatorAddress: delegatorAddr,
		TopicId:          topicId,
	}

	response, err := queryServer.GetStakeFromDelegatorInTopic(ctx, req)
	s.Require().NoError(err, "GetStakeFromDelegatorInTopic should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	expectedTotalStake := initialStakeAmount.Add(additionalStakeAmount)
	s.Require().Equal(expectedTotalStake, response.Amount, "The retrieved stake amount should match the total delegated stake")
}

func (s *QueryServerTestSuite) TestGetTopicStake() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()

	topicId := uint64(1)
	reputerAddr := s.AddrsStr(0)
	stakeAmount := cosmosMath.NewInt(500)

	err := keeper.AddReputerStake(ctx, topicId, reputerAddr, stakeAmount)
	s.Require().NoError(err)

	req := &types.GetTopicStakeRequest{
		TopicId: topicId,
	}

	response, err := queryServer.GetTopicStake(ctx, req)
	s.Require().NoError(err, "GetTopicStake should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(stakeAmount, response.Amount, "The retrieved topic stake should match the stake amount added")
}

func (s *QueryServerTestSuite) TestGetStakeRemovalInfo() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	blockHeight := int64(1234)
	topicId := uint64(1)
	address := s.AddrsStr(0)
	removal := types.StakeRemovalInfo{
		BlockRemovalStarted:   0,
		BlockRemovalCompleted: blockHeight,
		TopicId:               topicId,
		Reputer:               address,
		Amount:                cosmosMath.NewInt(100),
	}
	err := keeper.SetStakeRemoval(ctx, removal)
	s.Require().NoError(err, "SetStakeRemoval should not produce an error")
	req := &types.GetStakeRemovalInfoRequest{
		TopicId: topicId,
		Reputer: address,
	}
	response, err := queryServer.GetStakeRemovalInfo(ctx, req)
	s.Require().NoError(err, "GetStakeRemovalInfo should not produce an error")
	s.Require().NotNil(response, "The response should not be nil")
	s.Require().Equal(removal, *response.Removal, "The retrieved stake removal info should match the expected value")
}

func (s *QueryServerTestSuite) TestGetDelegateStakeRemovalInfo() {
	ctx := s.Ctx()
	queryServer := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	require := s.Require()
	blockHeight := int64(1234)
	topicId := uint64(1)
	delegatorAddress := s.AddrsStr(0)
	reputerAddress := s.AddrsStr(1)
	expectedRemoval := types.DelegateStakeRemovalInfo{
		BlockRemovalStarted:   0,
		BlockRemovalCompleted: blockHeight,
		TopicId:               topicId,
		Delegator:             delegatorAddress,
		Reputer:               reputerAddress,
		Amount:                cosmosMath.NewInt(100),
	}

	err := keeper.SetDelegateStakeRemoval(ctx, expectedRemoval)
	s.Require().NoError(err, "SetStakeRemoval should not produce an error")
	req := &types.GetDelegateStakeRemovalInfoRequest{
		TopicId:   topicId,
		Delegator: delegatorAddress,
		Reputer:   reputerAddress,
	}
	response, err := queryServer.GetDelegateStakeRemovalInfo(ctx, req)

	require.NoError(err, "GetDelegateStakeRemovalInfo should not produce an error")
	require.NotNil(response, "The response should not be nil")
	require.Equal(&expectedRemoval, response.Removal, "The retrieved stake removal info should match the expected value")
}

func (s *QueryServerTestSuite) TestGetStakeRemovalsForBlock() {
	ctx := s.Ctx()
	qs := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	require := s.Require()
	blockHeight := int64(1234)
	expecteds := []types.StakeRemovalInfo{
		{
			BlockRemovalStarted:   0,
			BlockRemovalCompleted: blockHeight,
			TopicId:               1,
			Reputer:               s.AddrsStr(0),
			Amount:                cosmosMath.NewInt(100),
		},
		{
			BlockRemovalStarted:   0,
			BlockRemovalCompleted: blockHeight,
			TopicId:               2,
			Reputer:               s.AddrsStr(1),
			Amount:                cosmosMath.NewInt(200),
		},
	}
	for _, e := range expecteds {
		err := keeper.SetStakeRemoval(ctx, e)
		require.NoError(err, "SetStakeRemoval should not produce an error")
	}

	req := &types.GetStakeRemovalsUpUntilBlockRequest{
		BlockHeight: blockHeight,
	}

	response, err := qs.GetStakeRemovalsUpUntilBlock(ctx, req)
	require.NoError(err, "GetStakeRemovalsForBlock should not return an error")
	require.NotNil(response, "The response should not be nil")
	require.Len(response.Removals, len(expecteds), "The number of stake removals should match the number of expected stake removals")
	require.Equal(expecteds[0], *response.Removals[0], "The retrieved stake removals should match the expected stake removals")
	require.Equal(expecteds[1], *response.Removals[1], "The retrieved stake removals should match the expected stake removals")
}

func (s *QueryServerTestSuite) TestGetDelegateStakeRemovalsForBlock() {
	ctx := s.Ctx()
	qs := s.EmissionsQueryServer()
	keeper := s.EmissionsKeeper()
	require := s.Require()
	blockHeight := int64(1234)
	expecteds := []types.DelegateStakeRemovalInfo{
		{
			BlockRemovalStarted:   0,
			BlockRemovalCompleted: blockHeight,
			TopicId:               1,
			Reputer:               s.AddrsStr(0),
			Delegator:             s.AddrsStr(1),
			Amount:                cosmosMath.NewInt(100),
		},
		{
			BlockRemovalStarted:   0,
			BlockRemovalCompleted: blockHeight,
			TopicId:               2,
			Reputer:               s.AddrsStr(2),
			Delegator:             s.AddrsStr(3),
			Amount:                cosmosMath.NewInt(200),
		},
	}
	for _, e := range expecteds {
		err := keeper.SetDelegateStakeRemoval(ctx, e)
		require.NoError(err, "SetStakeRemoval should not produce an error")
	}

	req := &types.GetDelegateStakeRemovalsUpUntilBlockRequest{
		BlockHeight: blockHeight,
	}

	response, err := qs.GetDelegateStakeRemovalsUpUntilBlock(ctx, req)
	require.NoError(err, "GetStakeRemovalsForBlock should not return an error")
	require.NotNil(response, "The response should not be nil")
	require.Len(response.Removals, len(expecteds), "The number of stake removals should match the number of expected stake removals")
	require.Equal(expecteds[0], *response.Removals[0], "The retrieved stake removals should match the expected stake removals")
	require.Equal(expecteds[1], *response.Removals[1], "The retrieved stake removals should match the expected stake removals")
}

func (s *QueryServerTestSuite) TestGetStakeReputerAuthority() {
	ctx := s.Ctx()
	keeper := s.EmissionsKeeper()
	topicId := uint64(1)
	reputerAddr := s.AddrsStr(0)
	stakeAmount := cosmosMath.NewInt(500)

	// Add stake
	err := keeper.AddReputerStake(ctx, topicId, reputerAddr, stakeAmount)
	s.Require().NoError(err)

	req := &types.GetStakeReputerAuthorityRequest{
		TopicId: topicId,
		Reputer: reputerAddr,
	}
	response, err := s.EmissionsQueryServer().GetStakeReputerAuthority(ctx, req)
	stakeAuthority := response.Authority

	s.Require().NoError(err)
	s.Require().Equal(stakeAmount, stakeAuthority, "Delegator stake should be equal to stake amount after addition")
}

func (s *QueryServerTestSuite) TestGetDelegateStakePlacement() {
	ctx := s.Ctx()
	require := s.Require()
	keeper := s.EmissionsKeeper()

	delegator := s.Addrs(0)
	delegatorAddr := s.AddrsStr(0)
	reputerAddr := s.AddrsStr(1)
	topicId := uint64(1)
	stakeAmount := cosmosMath.NewInt(50)
	s.MintTokensToAddress(delegator, cosmosMath.NewInt(1000))

	reputerInfo := types.OffchainNode{
		Owner:       s.AddrsStr(7),
		NodeAddress: s.AddrsStr(8),
	}

	err := keeper.InsertReputer(ctx, topicId, reputerAddr, reputerInfo)
	require.NoError(err)

	msg := &types.DelegateStakeRequest{
		Sender:  delegatorAddr,
		TopicId: topicId,
		Reputer: reputerAddr,
		Amount:  stakeAmount,
	}

	reputerStake, err := s.EmissionsKeeper().GetStakeReputerAuthority(ctx, topicId, reputerAddr)
	require.NoError(err)
	require.Equal(cosmosMath.ZeroInt(), reputerStake, "Stake amount mismatch")

	req := &types.GetDelegateStakePlacementRequest{
		TopicId:   topicId,
		Delegator: delegatorAddr,
		Target:    reputerAddr,
	}
	queryResponse, err := s.EmissionsQueryServer().GetDelegateStakePlacement(ctx, req)
	require.NoError(err)
	require.Equal(alloraMath.NewDecFromInt64(0), queryResponse.DelegatorInfo.Amount)

	// Perform the stake delegation
	response, err := s.EmissionsMsgServer().DelegateStake(ctx, msg)
	require.NoError(err)
	require.NotNil(response, "Response should not be nil after successful delegation")

	reputerStake, err = s.EmissionsKeeper().GetStakeReputerAuthority(ctx, topicId, reputerAddr)
	require.NoError(err)
	require.Equal(stakeAmount, reputerStake, "Stake amount mismatch")

	queryResponse, err = s.EmissionsQueryServer().GetDelegateStakePlacement(ctx, req)
	require.NoError(err)

	value0, err := strconv.ParseFloat(stakeAmount.ToLegacyDec().String(), 64)
	require.NoError(err)

	value1, err := strconv.ParseFloat(queryResponse.DelegatorInfo.Amount.String(), 64)
	require.NoError(err)

	require.Equal(value0, value1)
}

func (s *QueryServerTestSuite) TestGetDelegateStakeUponReputer() {
	ctx := s.Ctx()
	keeper := s.EmissionsKeeper()
	topicId := uint64(1)
	delegatorAddr := s.AddrsStr(0)
	reputerAddr := s.AddrsStr(1)
	initialStakeAmount := cosmosMath.NewInt(1000)
	removeStakeAmount := cosmosMath.NewInt(500)
	moduleParams, err := keeper.GetParams(ctx)
	s.Require().NoError(err)
	startBlock := ctx.BlockHeight()
	endBlock := startBlock + moduleParams.RemoveStakeDelayWindow

	// Setup initial stake
	err = keeper.AddDelegateStake(ctx, topicId, delegatorAddr, reputerAddr, initialStakeAmount)
	s.Require().NoError(err)

	// make a request to remove stake
	err = keeper.SetDelegateStakeRemoval(ctx, types.DelegateStakeRemovalInfo{
		BlockRemovalStarted:   startBlock,
		BlockRemovalCompleted: endBlock,
		TopicId:               topicId,
		Delegator:             delegatorAddr,
		Reputer:               reputerAddr,
		Amount:                removeStakeAmount,
	})
	s.Require().NoError(err)

	// Remove a portion of stake
	err = keeper.RemoveDelegateStake(ctx, endBlock, topicId, delegatorAddr, reputerAddr, removeStakeAmount)
	s.Require().NoError(err)

	// Check remaining stake for delegator
	remainingStake, err := keeper.GetStakeFromDelegatorInTopic(ctx, topicId, delegatorAddr)
	s.Require().NoError(err)
	expected := initialStakeAmount.Sub(removeStakeAmount)
	s.Require().Equal(expected, remainingStake, "Remaining delegator stake should be initial minus removed amount")

	// Check remaining stake for delegator
	stakeUponReputer, err := keeper.GetDelegateStakeUponReputer(ctx, topicId, reputerAddr)
	s.Require().NoError(err)
	s.Require().Equal(expected, stakeUponReputer, "Remaining reputer stake should be initial minus removed amount")
	req := &types.GetDelegateStakeUponReputerRequest{
		TopicId: topicId,
		Target:  reputerAddr,
	}
	queryResponse, err := s.EmissionsQueryServer().GetDelegateStakeUponReputer(ctx, req)
	stakeUponReputer = queryResponse.Stake
	s.Require().NoError(err)
	s.Require().Equal(expected, stakeUponReputer, "Remaining reputer stake should be initial minus removed amount")
}

func (s *QueryServerTestSuite) TestGetStakeRemovalForReputerAndTopicId() {
	k := s.EmissionsKeeper()
	ctx := s.Ctx()
	reputer := s.AddrsStr(2)
	topicId := uint64(1)

	// Create a stake removal info
	stakeRemovalInfo := types.StakeRemovalInfo{
		BlockRemovalStarted:   0,
		Reputer:               reputer,
		TopicId:               topicId,
		Amount:                cosmosMath.NewInt(100),
		BlockRemovalCompleted: 30,
	}
	anotherStakeRemoval := types.StakeRemovalInfo{
		BlockRemovalStarted:   0,
		Reputer:               s.AddrsStr(3),
		TopicId:               topicId,
		Amount:                cosmosMath.NewInt(200),
		BlockRemovalCompleted: 30,
	}

	// Set the stake removal info in the keeper
	err := k.SetStakeRemoval(ctx, stakeRemovalInfo)
	s.Require().NoError(err)
	err = k.SetStakeRemoval(ctx, anotherStakeRemoval)
	s.Require().NoError(err)

	req := &types.GetStakeRemovalForReputerAndTopicIdRequest{
		Reputer: reputer,
		TopicId: topicId,
	}
	response, err := s.EmissionsQueryServer().GetStakeRemovalForReputerAndTopicId(ctx, req)
	s.Require().NoError(err)
	s.Require().Equal(&stakeRemovalInfo, response.StakeRemovalInfo)
}
