package protocol

import (
	"context"
	"testing"

	"github.com/stretchr/testify/suite"
	"go.uber.org/zap"

	"github.com/ethereum/go-ethereum/crypto"
	gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
	"github.com/status-im/status-go/eth-node/types"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/communities"
	"github.com/status-im/status-go/protocol/protobuf"
	"github.com/status-im/status-go/protocol/requests"
	"github.com/status-im/status-go/protocol/tt"
	"github.com/status-im/status-go/waku"
)

func TestMessengerDeleteMessagesSuite(t *testing.T) {
	suite.Run(t, new(MessengerDeleteMessagesSuite))
}

type MessengerDeleteMessagesSuite struct {
	suite.Suite
	owner  *Messenger
	admin  *Messenger
	bob    *Messenger
	shh    types.Waku
	logger *zap.Logger
}

func (s *MessengerDeleteMessagesSuite) SetupTest() {
	s.logger = tt.MustCreateTestLogger()

	config := waku.DefaultConfig
	config.MinimumAcceptedPoW = 0
	shh := waku.New(&config, s.logger)
	s.shh = gethbridge.NewGethWakuWrapper(shh)
	s.Require().NoError(shh.Start())

	s.owner = s.newMessenger()
	s.bob = s.newMessenger()
	s.admin = s.newMessenger()
}

func (s *MessengerDeleteMessagesSuite) TearDownTest() {
	TearDownMessenger(&s.Suite, s.owner)
	TearDownMessenger(&s.Suite, s.bob)
	TearDownMessenger(&s.Suite, s.admin)
	_ = s.logger.Sync()
}

func (s *MessengerDeleteMessagesSuite) newMessenger() *Messenger {
	privateKey, err := crypto.GenerateKey()
	s.Require().NoError(err)

	messenger, err := newMessengerWithKey(s.shh, privateKey, s.logger, nil)
	s.Require().NoError(err)
	return messenger
}

func (s *MessengerDeleteMessagesSuite) sendMessageAndCheckDelivery(sender *Messenger, text string, chatID string) *common.Message {
	ctx := context.Background()
	messageToSend := common.NewMessage()
	messageToSend.ChatId = chatID
	messageToSend.ContentType = protobuf.ChatMessage_TEXT_PLAIN
	messageToSend.Text = text
	response, err := sender.SendChatMessage(ctx, messageToSend)
	s.Require().NoError(err)
	s.Require().Len(response.Messages(), 1)

	var message *common.Message
	if sender.identity != s.admin.identity {
		response, err := WaitOnMessengerResponse(s.admin, func(response *MessengerResponse) bool {
			return len(response.Messages()) == 1
		}, "admin did not receive message")
		s.Require().NoError(err)
		message = response.Messages()[0]
		s.Require().Equal(messageToSend.Text, message.Text)
	}

	if sender.identity != s.owner.identity {
		response, err = WaitOnMessengerResponse(s.owner, func(response *MessengerResponse) bool {
			return len(response.Messages()) == 1
		}, "owner did not receive message")
		s.Require().NoError(err)
		message = response.Messages()[0]
		s.Require().Equal(messageToSend.Text, message.Text)
	}

	if sender.identity != s.bob.identity {
		response, err = WaitOnMessengerResponse(s.bob, func(response *MessengerResponse) bool {
			return len(response.Messages()) == 1
		}, "bob did not receive message")
		s.Require().NoError(err)
		message = response.Messages()[0]
		s.Require().Equal(messageToSend.Text, message.Text)
	}

	return message
}

func (s *MessengerDeleteMessagesSuite) checkStoredMemberMessagesAmount(messenger *Messenger, memberPubKey string, expectedAmount int, communityID string) {
	storedMessages, err := messenger.GetCommunityMemberAllMessages(
		&requests.CommunityMemberMessages{
			CommunityID:     communityID,
			MemberPublicKey: memberPubKey})
	s.Require().NoError(err)
	s.Require().Len(storedMessages, expectedAmount)
}

func (s *MessengerDeleteMessagesSuite) checkAllMembersHasMemberMessages(memberPubKey string, expectedAmount int, communityID string) {
	s.checkStoredMemberMessagesAmount(s.bob, memberPubKey, expectedAmount, communityID)
	s.checkStoredMemberMessagesAmount(s.owner, memberPubKey, expectedAmount, communityID)
	s.checkStoredMemberMessagesAmount(s.admin, memberPubKey, expectedAmount, communityID)
}

func (s *MessengerDeleteMessagesSuite) TestDeleteMessageErrors() {
	community, communityChat := createCommunity(&s.Suite, s.owner)

	request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}

	advertiseCommunityTo(&s.Suite, community, s.owner, s.admin)
	joinCommunity(&s.Suite, community, s.owner, s.admin, request, "")

	advertiseCommunityTo(&s.Suite, community, s.owner, s.bob)
	joinCommunity(&s.Suite, community, s.owner, s.bob, request, "")

	grantPermission(&s.Suite, community, s.owner, s.admin, protobuf.CommunityMember_ROLE_ADMIN)

	bobMessage := s.sendMessageAndCheckDelivery(s.bob, "bob message", communityChat.ID)

	expectedMsgsToRemove := 1
	communityID := community.IDString()
	s.checkAllMembersHasMemberMessages(s.bob.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// empty request
	deleteMessagesRequest := &requests.DeleteCommunityMemberMessages{}
	_, err := s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidCommunityID)

	// only community ID provided
	deleteMessagesRequest.CommunityID = community.ID()
	_, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidMemberID)

	// only community ID and member ID provided, but delete flag false and no messages IDs
	deleteMessagesRequest.MemberPubKey = s.bob.IdentityPublicKeyString()
	_, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidDeleteMessagesByID)

	// message provided without id
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		ChatId: bobMessage.ChatId,
	}}

	_, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidMsgID)

	// message provided without chatId
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		Id: bobMessage.ID,
	}}
	_, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidMsgChatID)

	// messages id provided but with flag deleteAll
	deleteMessagesRequest.CommunityID = community.ID()
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		Id:     bobMessage.ID,
		ChatId: bobMessage.ChatId,
	}}
	deleteMessagesRequest.DeleteAll = true
	_, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, requests.ErrDeleteCommunityMemberMessagesInvalidDeleteAll)

	// bob tries to delete his own message
	deleteMessagesRequest.DeleteAll = false
	_, err = s.bob.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, communities.ErrNotEnoughPermissions)

	// admin tries to delete owner message
	deleteMessagesRequest.MemberPubKey = s.owner.IdentityPublicKeyString()
	_, err = s.admin.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().ErrorIs(err, communities.ErrNotOwner)
}

func (s *MessengerDeleteMessagesSuite) TestDeleteMessage() {
	community, communityChat := createCommunity(&s.Suite, s.owner)

	request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}

	advertiseCommunityTo(&s.Suite, community, s.owner, s.admin)
	joinCommunity(&s.Suite, community, s.owner, s.admin, request, "")

	advertiseCommunityTo(&s.Suite, community, s.owner, s.bob)
	joinCommunity(&s.Suite, community, s.owner, s.bob, request, "")

	grantPermission(&s.Suite, community, s.owner, s.admin, protobuf.CommunityMember_ROLE_ADMIN)

	bobMessage := s.sendMessageAndCheckDelivery(s.bob, "bob message", communityChat.ID)
	bobMessage2 := s.sendMessageAndCheckDelivery(s.bob, "bob message2", communityChat.ID)
	ownerMessage := s.sendMessageAndCheckDelivery(s.owner, "owner message", communityChat.ID)
	adminMessage := s.sendMessageAndCheckDelivery(s.admin, "admin message", communityChat.ID)

	identityString := s.bob.IdentityPublicKeyString()
	expectedMsgsToRemove := 2
	communityID := community.IDString()
	s.checkAllMembersHasMemberMessages(identityString, expectedMsgsToRemove, communityID)
	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), 1, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), 1, communityID)

	// delete bob message
	deleteMessagesRequest := &requests.DeleteCommunityMemberMessages{
		CommunityID:  community.ID(),
		MemberPubKey: identityString,
		Messages: []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
			Id:     bobMessage.ID,
			ChatId: bobMessage.ChatId,
		}},
	}
	response, err := s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().NoError(err)

	checkMessageDeleted := func(response *MessengerResponse) bool {
		if len(response.DeletedMessages()) == 0 {
			return false
		}

		if _, exists := response.DeletedMessages()[deleteMessagesRequest.Messages[0].Id]; !exists {
			return false
		}
		return true
	}

	s.Require().True(checkMessageDeleted(response))

	_, err = WaitOnMessengerResponse(s.bob, checkMessageDeleted, "message was not deleted for bob")
	s.Require().NoError(err)
	_, err = WaitOnMessengerResponse(s.admin, checkMessageDeleted, "message was not deleted for admin")
	s.Require().NoError(err)

	expectedMsgsToRemove = 1
	s.checkAllMembersHasMemberMessages(s.bob.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// check that other users messages were not removed
	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// check that admin can delete member message
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		Id:     bobMessage2.ID,
		ChatId: bobMessage2.ChatId,
	}}
	response, err = s.admin.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().NoError(err)
	s.Require().True(checkMessageDeleted(response))

	_, err = WaitOnMessengerResponse(s.bob, checkMessageDeleted, "message2 was not deleted for bob")
	s.Require().NoError(err)
	_, err = WaitOnMessengerResponse(s.owner, checkMessageDeleted, "message2 was not deleted for owner")
	s.Require().NoError(err)

	expectedMsgsToRemove = 0
	s.checkAllMembersHasMemberMessages(s.bob.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// check that other users messages were not removed
	expectedMsgsToRemove = 1
	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// check that owner can delete member message
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		Id:     adminMessage.ID,
		ChatId: adminMessage.ChatId,
	}}
	response, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().NoError(err)
	s.Require().True(checkMessageDeleted(response))

	_, err = WaitOnMessengerResponse(s.bob, checkMessageDeleted, "adminMessage was not deleted for bob")
	s.Require().NoError(err)
	_, err = WaitOnMessengerResponse(s.admin, checkMessageDeleted, "adminMessage was not deleted for admin")
	s.Require().NoError(err)

	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), 0, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), 1, communityID)

	// check that owner can delete his own message
	deleteMessagesRequest.Messages = []*protobuf.DeleteCommunityMemberMessage{&protobuf.DeleteCommunityMemberMessage{
		Id:     ownerMessage.ID,
		ChatId: ownerMessage.ChatId,
	}}
	response, err = s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().NoError(err)
	s.Require().True(checkMessageDeleted(response))

	_, err = WaitOnMessengerResponse(s.bob, checkMessageDeleted, "ownerMessage was not deleted for bob")
	s.Require().NoError(err)
	_, err = WaitOnMessengerResponse(s.admin, checkMessageDeleted, "ownerMessage was not deleted for admin")
	s.Require().NoError(err)

	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), 0, communityID)
}

func (s *MessengerDeleteMessagesSuite) TestDeleteAllMemberMessage() {
	community, communityChat := createCommunity(&s.Suite, s.owner)

	request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}

	advertiseCommunityTo(&s.Suite, community, s.owner, s.admin)
	joinCommunity(&s.Suite, community, s.owner, s.admin, request, "")

	advertiseCommunityTo(&s.Suite, community, s.owner, s.bob)
	joinCommunity(&s.Suite, community, s.owner, s.bob, request, "")

	grantPermission(&s.Suite, community, s.owner, s.admin, protobuf.CommunityMember_ROLE_ADMIN)

	_ = s.sendMessageAndCheckDelivery(s.bob, "bob message", communityChat.ID)
	_ = s.sendMessageAndCheckDelivery(s.bob, "bob message2", communityChat.ID)
	_ = s.sendMessageAndCheckDelivery(s.owner, "owner message", communityChat.ID)
	_ = s.sendMessageAndCheckDelivery(s.admin, "admin message", communityChat.ID)

	identityString := s.bob.IdentityPublicKeyString()
	expectedMsgsToRemove := 2
	communityID := community.IDString()
	s.checkAllMembersHasMemberMessages(identityString, expectedMsgsToRemove, communityID)
	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), 1, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), 1, communityID)

	// delete all bob message
	deleteMessagesRequest := &requests.DeleteCommunityMemberMessages{
		CommunityID:  community.ID(),
		MemberPubKey: identityString,
		DeleteAll:    true,
	}
	response, err := s.owner.DeleteCommunityMemberMessages(deleteMessagesRequest)
	s.Require().NoError(err)

	checkMessageDeleted := func(response *MessengerResponse) bool {
		return len(response.DeletedMessages()) == 2
	}

	s.Require().True(checkMessageDeleted(response))

	_, err = WaitOnMessengerResponse(s.bob, checkMessageDeleted, "messages were not deleted for bob")
	s.Require().NoError(err)
	_, err = WaitOnMessengerResponse(s.admin, checkMessageDeleted, "messages were not deleted for admin")
	s.Require().NoError(err)

	expectedMsgsToRemove = 0
	s.checkAllMembersHasMemberMessages(s.bob.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)

	// check that other users messages were not removed
	expectedMsgsToRemove = 1
	s.checkAllMembersHasMemberMessages(s.admin.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)
	s.checkAllMembersHasMemberMessages(s.owner.IdentityPublicKeyString(), expectedMsgsToRemove, communityID)
}