package protocol

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"testing"

	"github.com/golang/protobuf/proto"
	"github.com/stretchr/testify/suite"

	userimage "github.com/status-im/status-go/images"
	multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"

	"github.com/status-im/status-go/multiaccounts/settings"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/protobuf"
)

func TestGroupChatSuite(t *testing.T) {
	suite.Run(t, new(MessengerGroupChatSuite))
}

type MessengerGroupChatSuite struct {
	MessengerBaseTestSuite
}

func (s *MessengerGroupChatSuite) createGroupChat(creator *Messenger, name string, members []string) *Chat {
	response, err := creator.CreateGroupChatWithMembers(context.Background(), name, members)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)

	chat := response.Chats()[0]
	err = creator.SaveChat(chat)
	s.Require().NoError(err)

	return chat
}

func (s *MessengerGroupChatSuite) createEmptyGroupChat(creator *Messenger, name string) *Chat {
	return s.createGroupChat(creator, name, []string{})
}

func (s *MessengerGroupChatSuite) verifyGroupChatCreated(member *Messenger, expectedChatActive bool) {
	response, err := WaitOnMessengerResponse(
		member,
		func(r *MessengerResponse) bool {
			return len(r.Chats()) == 1 && r.Chats()[0].Active == expectedChatActive
		},
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().True(response.Chats()[0].Active == expectedChatActive)
}

func makeMutualContact(origin *Messenger, contactPubkey *ecdsa.PublicKey) error {
	contact, err := BuildContactFromPublicKey(contactPubkey)
	if err != nil {
		return err
	}
	contact.ContactRequestLocalState = ContactRequestStateSent
	contact.ContactRequestRemoteState = ContactRequestStateReceived
	origin.allContacts.Store(contact.ID, contact)

	return nil
}

func (s *MessengerGroupChatSuite) makeContact(origin *Messenger, toAdd *Messenger) {
	s.Require().NoError(makeMutualContact(origin, &toAdd.identity.PublicKey))
}

func (s *MessengerGroupChatSuite) makeMutualContacts(lhs *Messenger, rhs *Messenger) {
	s.makeContact(lhs, rhs)
	s.makeContact(rhs, lhs)
}

func (s *MessengerGroupChatSuite) TestGroupChatCreation() {
	testCases := []struct {
		name                          string
		creatorAddedMemberAsContact   bool
		memberAddedCreatorAsContact   bool
		expectedCreationSuccess       bool
		expectedAddedMemberChatActive bool
	}{
		{
			name:                          "not added - not added",
			creatorAddedMemberAsContact:   false,
			memberAddedCreatorAsContact:   false,
			expectedCreationSuccess:       false,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "added - not added",
			creatorAddedMemberAsContact:   true,
			memberAddedCreatorAsContact:   false,
			expectedCreationSuccess:       true,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "not added - added",
			creatorAddedMemberAsContact:   false,
			memberAddedCreatorAsContact:   true,
			expectedCreationSuccess:       false,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "added - added",
			creatorAddedMemberAsContact:   true,
			memberAddedCreatorAsContact:   true,
			expectedCreationSuccess:       true,
			expectedAddedMemberChatActive: true,
		},
	}

	for i, testCase := range testCases {
		creator := s.newMessenger()
		member := s.newMessenger()
		members := []string{common.PubkeyToHex(&member.identity.PublicKey)}

		if testCase.creatorAddedMemberAsContact {
			s.makeContact(creator, member)
		}
		if testCase.memberAddedCreatorAsContact {
			s.makeContact(member, creator)
		}

		_, err := creator.CreateGroupChatWithMembers(context.Background(), fmt.Sprintf("test_group_chat_%d", i), members)
		if testCase.creatorAddedMemberAsContact {
			s.Require().NoError(err)
			s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive)
		} else {
			s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts")
		}

		defer s.NoError(creator.Shutdown())
		defer s.NoError(member.Shutdown())
	}
}

func (s *MessengerGroupChatSuite) TestGroupChatMembersAddition() {
	testCases := []struct {
		name                          string
		inviterAddedMemberAsContact   bool
		memberAddedInviterAsContact   bool
		expectedAdditionSuccess       bool
		expectedAddedMemberChatActive bool
	}{
		{
			name:                          "not added - not added",
			inviterAddedMemberAsContact:   false,
			memberAddedInviterAsContact:   false,
			expectedAdditionSuccess:       false,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "added - not added",
			inviterAddedMemberAsContact:   true,
			memberAddedInviterAsContact:   false,
			expectedAdditionSuccess:       true,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "not added - added",
			inviterAddedMemberAsContact:   false,
			memberAddedInviterAsContact:   true,
			expectedAdditionSuccess:       false,
			expectedAddedMemberChatActive: false,
		},
		{
			name:                          "added - added",
			inviterAddedMemberAsContact:   true,
			memberAddedInviterAsContact:   true,
			expectedAdditionSuccess:       true,
			expectedAddedMemberChatActive: true,
		},
	}

	for i, testCase := range testCases {
		admin := s.newMessenger()
		inviter := s.newMessenger()
		member := s.newMessenger()
		members := []string{common.PubkeyToHex(&member.identity.PublicKey)}

		if testCase.inviterAddedMemberAsContact {
			s.makeContact(inviter, member)
		}
		if testCase.memberAddedInviterAsContact {
			s.makeContact(member, inviter)
		}

		for j, inviterIsAlsoGroupCreator := range []bool{true, false} {
			var groupChat *Chat
			if inviterIsAlsoGroupCreator {
				groupChat = s.createEmptyGroupChat(inviter, fmt.Sprintf("test_group_chat_%d_%d", i, j))
			} else {
				s.makeContact(admin, inviter)
				groupChat = s.createGroupChat(admin, fmt.Sprintf("test_group_chat_%d_%d", i, j), []string{common.PubkeyToHex(&inviter.identity.PublicKey)})
				err := inviter.SaveChat(groupChat)
				s.Require().NoError(err)
			}

			_, err := inviter.AddMembersToGroupChat(context.Background(), groupChat.ID, members)
			if testCase.inviterAddedMemberAsContact {
				s.Require().NoError(err)
				s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive)
			} else {
				s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts")
			}
		}

		defer s.NoError(admin.Shutdown())
		defer s.NoError(inviter.Shutdown())
		defer s.NoError(member.Shutdown())
	}
}

func (s *MessengerGroupChatSuite) TestGroupChatMembersRemoval() {
	admin := s.newMessenger()
	memberA := s.newMessenger()
	memberB := s.newMessenger()
	memberC := s.newMessenger()
	members := []string{common.PubkeyToHex(&memberA.identity.PublicKey), common.PubkeyToHex(&memberB.identity.PublicKey),
		common.PubkeyToHex(&memberC.identity.PublicKey)}

	s.makeMutualContacts(admin, memberA)
	s.makeMutualContacts(admin, memberB)
	s.makeMutualContacts(admin, memberC)

	groupChat := s.createGroupChat(admin, "test_group_chat", members)
	s.verifyGroupChatCreated(memberA, true)
	s.verifyGroupChatCreated(memberB, true)
	s.verifyGroupChatCreated(memberC, true)

	_, err := memberA.RemoveMembersFromGroupChat(context.Background(), groupChat.ID, []string{common.PubkeyToHex(&memberB.identity.PublicKey),
		common.PubkeyToHex(&memberC.identity.PublicKey)})
	s.Require().Error(err)

	// only admin can remove members from the group
	_, err = admin.RemoveMembersFromGroupChat(context.Background(), groupChat.ID, []string{common.PubkeyToHex(&memberB.identity.PublicKey),
		common.PubkeyToHex(&memberC.identity.PublicKey)})
	s.Require().NoError(err)

	// ensure removal is propagated to other members
	response, err := WaitOnMessengerResponse(
		memberA,
		func(r *MessengerResponse) bool { return len(r.Chats()) > 0 },
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().True(response.Chats()[0].Active)
	s.Require().Len(response.Chats()[0].Members, 2)

	defer s.NoError(admin.Shutdown())
	defer s.NoError(memberA.Shutdown())
	defer s.NoError(memberB.Shutdown())
	defer s.NoError(memberC.Shutdown())
}

func (s *MessengerGroupChatSuite) TestGroupChatEdit() {
	admin := s.newMessenger()
	member := s.newMessenger()
	s.makeMutualContacts(admin, member)

	groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)})
	s.verifyGroupChatCreated(member, true)

	response, err := admin.EditGroupChat(context.Background(), groupChat.ID, "test_admin_group", "#FF00FF", userimage.CroppedImage{})
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().Equal("test_admin_group", response.Chats()[0].Name)
	s.Require().Equal("#FF00FF", response.Chats()[0].Color)
	// TODO: handle image

	// ensure group edit is propagated to other members
	response, err = WaitOnMessengerResponse(
		member,
		func(r *MessengerResponse) bool { return len(r.Chats()) > 0 },
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().Equal("test_admin_group", response.Chats()[0].Name)
	s.Require().Equal("#FF00FF", response.Chats()[0].Color)

	response, err = member.EditGroupChat(context.Background(), groupChat.ID, "test_member_group", "#F0F0F0", userimage.CroppedImage{})
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().Equal("test_member_group", response.Chats()[0].Name)
	s.Require().Equal("#F0F0F0", response.Chats()[0].Color)

	// ensure group edit is propagated to other members
	response, err = WaitOnMessengerResponse(
		admin,
		func(r *MessengerResponse) bool { return len(r.Chats()) > 0 },
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().Equal("test_member_group", response.Chats()[0].Name)
	s.Require().Equal("#F0F0F0", response.Chats()[0].Color)

	inputMessage := buildTestMessage(*groupChat)

	_, err = admin.SendChatMessage(context.Background(), inputMessage)
	s.Require().NoError(err)

	response, err = WaitOnMessengerResponse(
		member,
		func(r *MessengerResponse) bool { return len(r.Messages()) > 0 },
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Messages(), 1)
	s.Require().Equal(inputMessage.Text, response.Messages()[0].Text)

	defer s.NoError(admin.Shutdown())
	defer s.NoError(member.Shutdown())
}

func (s *MessengerGroupChatSuite) TestGroupChatDeleteMemberMessage() {
	admin := s.newMessenger()
	member := s.newMessenger()
	s.makeMutualContacts(admin, member)

	groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)})
	s.verifyGroupChatCreated(member, true)

	ctx := context.Background()
	inputMessage := buildTestMessage(*groupChat)
	_, err := member.SendChatMessage(ctx, inputMessage)
	s.Require().NoError(err)

	response, err := WaitOnMessengerResponse(
		admin,
		func(r *MessengerResponse) bool { return len(r.Messages()) > 0 },
		"messages not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Messages(), 1)
	s.Require().Equal(inputMessage.Text, response.Messages()[0].Text)

	message := response.Messages()[0]
	deleteMessageResponse, err := admin.DeleteMessageAndSend(ctx, message.ID)
	s.Require().NoError(err)

	_, err = WaitOnMessengerResponse(member, func(response *MessengerResponse) bool {
		return len(response.RemovedMessages()) > 0
	}, "removed messages not received")
	s.Require().Equal(deleteMessageResponse.RemovedMessages()[0].DeletedBy, contactIDFromPublicKey(admin.IdentityPublicKey()))
	s.Require().NoError(err)
	message, err = member.MessageByID(message.ID)
	s.Require().NoError(err)
	s.Require().True(message.Deleted)

	defer s.NoError(admin.Shutdown())
	defer s.NoError(member.Shutdown())
}

func (s *MessengerGroupChatSuite) TestGroupChatHandleDeleteMemberMessage() {
	admin := s.newMessenger()
	member := s.newMessenger()
	s.makeMutualContacts(admin, member)

	groupChat := s.createGroupChat(admin, "test_group_chat", []string{common.PubkeyToHex(&member.identity.PublicKey)})
	s.verifyGroupChatCreated(member, true)

	ctx := context.Background()
	inputMessage := buildTestMessage(*groupChat)
	_, err := member.SendChatMessage(ctx, inputMessage)
	s.Require().NoError(err)

	response, err := WaitOnMessengerResponse(
		admin,
		func(r *MessengerResponse) bool { return len(r.Messages()) > 0 },
		"messages not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Messages(), 1)
	s.Require().Equal(inputMessage.Text, response.Messages()[0].Text)

	deleteMessage := &DeleteMessage{
		DeleteMessage: &protobuf.DeleteMessage{
			Clock:       2,
			MessageType: protobuf.MessageType_PRIVATE_GROUP,
			MessageId:   inputMessage.ID,
			ChatId:      groupChat.ID,
		},
		From: common.PubkeyToHex(&admin.identity.PublicKey),
	}

	state := &ReceivedMessageState{
		Response: &MessengerResponse{},
	}

	err = member.handleDeleteMessage(state, deleteMessage)
	s.Require().NoError(err)

	removedMessages := state.Response.RemovedMessages()
	s.Require().Len(removedMessages, 1)
	s.Require().Equal(removedMessages[0].MessageID, inputMessage.ID)

	defer s.NoError(admin.Shutdown())
	defer s.NoError(member.Shutdown())
}

func (s *MessengerGroupChatSuite) TestGroupChatMembersRemovalOutOfOrder() {
	admin := s.newMessenger()
	memberA := s.newMessenger()
	members := []string{common.PubkeyToHex(&memberA.identity.PublicKey)}

	s.makeMutualContacts(admin, memberA)

	groupChat := s.createGroupChat(admin, "test_group_chat", members)

	removeMembersResponse, err := admin.removeMembersFromGroupChat(context.Background(), groupChat, []string{common.PubkeyToHex(&memberA.identity.PublicKey)})
	s.Require().NoError(err)

	encodedMessage := removeMembersResponse.encodedProtobuf

	message := protobuf.MembershipUpdateMessage{}
	err = proto.Unmarshal(encodedMessage, &message)
	s.Require().NoError(err)

	response := &MessengerResponse{}

	messageState := &ReceivedMessageState{
		ExistingMessagesMap: make(map[string]bool),
		Response:            response,
		AllChats:            new(chatMap),
		Timesource:          memberA.getTimesource(),
	}

	c, err := buildContact(admin.myHexIdentity(), &admin.identity.PublicKey)
	s.Require().NoError(err)

	messageState.CurrentMessageState = &CurrentMessageState{
		Contact: c,
	}

	err = memberA.HandleMembershipUpdate(messageState, nil, &message, memberA.systemMessagesTranslations)

	s.Require().NoError(err)
	s.Require().NotNil(messageState.Response)
	s.Require().Len(messageState.Response.Chats(), 1)
	s.Require().Len(messageState.Response.Chats()[0].Members, 1)
	defer s.NoError(admin.Shutdown())
	defer s.NoError(memberA.Shutdown())
}

func (s *MessengerGroupChatSuite) TestGroupChatMembersInfoSync() {
	admin, memberA, memberB := s.newMessenger(), s.newMessenger(), s.newMessenger()
	memberB.account.CustomizationColor = multiaccountscommon.CustomizationColorBlue
	s.Require().NoError(admin.settings.SaveSettingField(settings.DisplayName, "admin"))
	s.Require().NoError(memberA.settings.SaveSettingField(settings.DisplayName, "memberA"))
	s.Require().NoError(memberB.settings.SaveSettingField(settings.DisplayName, "memberB"))

	members := []string{common.PubkeyToHex(&memberA.identity.PublicKey), common.PubkeyToHex(&memberB.identity.PublicKey)}

	s.makeMutualContacts(admin, memberA)
	s.makeMutualContacts(admin, memberB)

	s.createGroupChat(admin, "test_group_chat", members)
	s.verifyGroupChatCreated(memberA, true)
	s.verifyGroupChatCreated(memberB, true)

	response, err := WaitOnMessengerResponse(
		memberA,
		func(r *MessengerResponse) bool { return len(r.Chats()) > 0 },
		"chat invitation not received",
	)
	s.Require().NoError(err)
	s.Require().Len(response.Chats(), 1)
	s.Require().True(response.Chats()[0].Active)
	s.Require().Len(response.Chats()[0].Members, 3)

	_, err = WaitOnMessengerResponse(
		memberA,
		func(r *MessengerResponse) bool {
			// we republish as we don't have store nodes in tests
			err := memberB.publishContactCode()
			if err != nil {
				return false
			}
			contact, ok := memberA.allContacts.Load(common.PubkeyToHex(&memberB.identity.PublicKey))
			return ok && contact.DisplayName == "memberB" && contact.CustomizationColor == memberB.account.GetCustomizationColor()
		},
		"DisplayName is not the same",
	)
	s.Require().NoError(err)

	s.NoError(admin.Shutdown())
	s.NoError(memberA.Shutdown())
	s.NoError(memberB.Shutdown())
}