package ws

import (
	"sync"
	"testing"

	"github.com/mattermost/focalboard/server/model"

	mmModel "github.com/mattermost/mattermost-server/v6/model"

	"github.com/stretchr/testify/require"
)

func TestPluginAdapterTeamSubscription(t *testing.T) {
	th := SetupTestHelper(t)

	webConnID := mmModel.NewId()
	userID := mmModel.NewId()
	teamID := mmModel.NewId()

	var pac *PluginAdapterClient
	t.Run("Should correctly add a connection", func(t *testing.T) {
		require.Empty(t, th.pa.listeners)
		require.Empty(t, th.pa.listenersByTeam)
		th.pa.OnWebSocketConnect(webConnID, userID)
		require.Len(t, th.pa.listeners, 1)

		var ok bool
		pac, ok = th.pa.listeners[webConnID]
		require.True(t, ok)
		require.NotNil(t, pac)
		require.Equal(t, userID, pac.userID)
		require.Empty(t, th.pa.listenersByTeam)
	})

	t.Run("Should correctly subscribe to a team", func(t *testing.T) {
		require.False(t, pac.isSubscribedToTeam(teamID))

		th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)

		require.Len(t, th.pa.listenersByTeam[teamID], 1)
		require.Contains(t, th.pa.listenersByTeam[teamID], pac)
		require.Len(t, pac.teams, 1)
		require.Contains(t, pac.teams, teamID)

		require.True(t, pac.isSubscribedToTeam(teamID))
	})

	t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) {
		require.True(t, pac.isSubscribedToTeam(teamID))

		th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)

		require.Len(t, th.pa.listenersByTeam[teamID], 1)
		require.Contains(t, th.pa.listenersByTeam[teamID], pac)
		require.Len(t, pac.teams, 1)
		require.Contains(t, pac.teams, teamID)

		require.True(t, pac.isSubscribedToTeam(teamID))
	})

	t.Run("Should correctly unsubscribe to a team", func(t *testing.T) {
		require.True(t, pac.isSubscribedToTeam(teamID))

		th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID)

		require.Empty(t, th.pa.listenersByTeam[teamID])
		require.Empty(t, pac.teams)

		require.False(t, pac.isSubscribedToTeam(teamID))
	})

	t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) {
		require.False(t, pac.isSubscribedToTeam(teamID))

		th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID)

		require.Empty(t, th.pa.listenersByTeam[teamID])
		require.Empty(t, pac.teams)

		require.False(t, pac.isSubscribedToTeam(teamID))
	})

	t.Run("Should correctly be marked as inactive if disconnected", func(t *testing.T) {
		require.Len(t, th.pa.listeners, 1)
		require.True(t, th.pa.listeners[webConnID].isActive())

		th.pa.OnWebSocketDisconnect(webConnID, userID)

		require.Len(t, th.pa.listeners, 1)
		require.False(t, th.pa.listeners[webConnID].isActive())
	})

	t.Run("Should be marked back as active if reconnect", func(t *testing.T) {
		require.Len(t, th.pa.listeners, 1)
		require.False(t, th.pa.listeners[webConnID].isActive())

		th.pa.OnWebSocketConnect(webConnID, userID)

		require.Len(t, th.pa.listeners, 1)
		require.True(t, th.pa.listeners[webConnID].isActive())
	})
}

func TestPluginAdapterClientReconnect(t *testing.T) {
	th := SetupTestHelper(t)

	webConnID := mmModel.NewId()
	userID := mmModel.NewId()
	teamID := mmModel.NewId()

	var pac *PluginAdapterClient
	t.Run("A user should be able to reconnect within the accepted threshold and keep their subscriptions", func(t *testing.T) {
		// create the connection
		require.Len(t, th.pa.listeners, 0)
		require.Len(t, th.pa.listenersByUserID[userID], 0)
		th.pa.OnWebSocketConnect(webConnID, userID)
		require.Len(t, th.pa.listeners, 1)
		require.Len(t, th.pa.listenersByUserID[userID], 1)
		var ok bool
		pac, ok = th.pa.listeners[webConnID]
		require.True(t, ok)
		require.NotNil(t, pac)

		th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)
		require.True(t, pac.isSubscribedToTeam(teamID))

		// disconnect
		th.pa.OnWebSocketDisconnect(webConnID, userID)
		require.False(t, pac.isActive())
		require.Len(t, th.pa.listeners, 1)
		require.Len(t, th.pa.listenersByUserID[userID], 1)

		// reconnect right away. The connection should still be subscribed
		th.pa.OnWebSocketConnect(webConnID, userID)
		require.Len(t, th.pa.listeners, 1)
		require.Len(t, th.pa.listenersByUserID[userID], 1)
		require.True(t, pac.isActive())
		require.True(t, pac.isSubscribedToTeam(teamID))
	})

	t.Run("Should remove old inactive connection when user connects with a different ID", func(t *testing.T) {
		// we set the stale threshold to zero so inactive connections always get deleted
		oldStaleThreshold := th.pa.staleThreshold
		th.pa.staleThreshold = 0
		defer func() { th.pa.staleThreshold = oldStaleThreshold }()
		th.pa.OnWebSocketDisconnect(webConnID, userID)
		require.Len(t, th.pa.listeners, 1)
		require.Len(t, th.pa.listenersByUserID[userID], 1)
		require.Equal(t, webConnID, th.pa.listenersByUserID[userID][0].webConnID)

		newWebConnID := mmModel.NewId()
		th.pa.OnWebSocketConnect(newWebConnID, userID)

		require.Len(t, th.pa.listeners, 1)
		require.Len(t, th.pa.listenersByUserID[userID], 1)
		require.Contains(t, th.pa.listeners, newWebConnID)
		require.NotContains(t, th.pa.listeners, webConnID)
		require.Equal(t, newWebConnID, th.pa.listenersByUserID[userID][0].webConnID)

		// if the same ID connects again, it should have no subscriptions
		th.pa.OnWebSocketConnect(webConnID, userID)
		require.Len(t, th.pa.listeners, 2)
		require.Len(t, th.pa.listenersByUserID[userID], 2)
		reconnectedPAC, ok := th.pa.listeners[webConnID]
		require.True(t, ok)
		require.False(t, reconnectedPAC.isSubscribedToTeam(teamID))
	})

	t.Run("Should not remove active connections when user connects with a different ID", func(t *testing.T) {
		// we set the stale threshold to zero so inactive connections always get deleted
		oldStaleThreshold := th.pa.staleThreshold
		th.pa.staleThreshold = 0
		defer func() { th.pa.staleThreshold = oldStaleThreshold }()

		// currently we have two listeners for userID, both active
		require.Len(t, th.pa.listeners, 2)

		// a new user connects
		th.pa.OnWebSocketConnect(mmModel.NewId(), userID)

		// and we should have three connections, all of them active
		require.Len(t, th.pa.listeners, 3)

		for _, listener := range th.pa.listeners {
			require.True(t, listener.isActive())
		}
	})
}

func TestGetUserIDsForTeam(t *testing.T) {
	th := SetupTestHelper(t)

	// we have two teams
	teamID1 := mmModel.NewId()
	teamID2 := mmModel.NewId()

	// user 1 has two connections
	userID1 := mmModel.NewId()
	webConnID1 := mmModel.NewId()
	webConnID2 := mmModel.NewId()

	// user 2 has one connection
	userID2 := mmModel.NewId()
	webConnID3 := mmModel.NewId()

	wg := new(sync.WaitGroup)
	wg.Add(3)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID1, userID1)
		th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1)
		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID2, userID1)
		th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2)
		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID3, userID2)
		th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2)
		wg.Done()
	}(wg)

	wg.Wait()

	t.Run("should find that only user1 is connected to team 1", func(t *testing.T) {
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID1).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID1)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})

	t.Run("should find that both users are connected to team 2", func(t *testing.T) {
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID2)
		require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
	})

	t.Run("should ignore user1 if webConn 2 inactive when getting team 2 user ids", func(t *testing.T) {
		th.pa.OnWebSocketDisconnect(webConnID2, userID1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID2)
		require.ElementsMatch(t, []string{userID2}, userIDs)
	})

	t.Run("should still find user 1 in team 1 after the webConn 2 disconnection", func(t *testing.T) {
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID1).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID1)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})

	t.Run("should find again both users if the webConn 2 comes back", func(t *testing.T) {
		th.pa.OnWebSocketConnect(webConnID2, userID1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID2)
		require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
	})

	t.Run("should only find user 1 if user 2 has an active connection but is not a team member anymore", func(t *testing.T) {
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)

		// userID2 does not have team access
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(false).
			Times(1)

		userIDs := th.pa.getUserIDsForTeam(teamID2)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})
}

func TestGetUserIDsForTeamAndBoard(t *testing.T) {
	th := SetupTestHelper(t)

	// we have two teams
	teamID1 := mmModel.NewId()
	boardID1 := mmModel.NewId()
	teamID2 := mmModel.NewId()
	boardID2 := mmModel.NewId()

	// user 1 has two connections
	userID1 := mmModel.NewId()
	webConnID1 := mmModel.NewId()
	webConnID2 := mmModel.NewId()

	// user 2 has one connection
	userID2 := mmModel.NewId()
	webConnID3 := mmModel.NewId()

	wg := new(sync.WaitGroup)
	wg.Add(3)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID1, userID1)
		th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1)
		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID2, userID1)
		th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2)
		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.pa.OnWebSocketConnect(webConnID3, userID2)
		th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2)
		wg.Done()
	}(wg)

	wg.Wait()

	t.Run("should find that only user1 is connected to team 1 and board 1", func(t *testing.T) {
		mockedMembers := []*model.BoardMember{{UserID: userID1}}
		th.store.EXPECT().
			GetMembersForBoard(boardID1).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID1).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID1, boardID1)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})

	t.Run("should find that both users are connected to team 2 and board 2", func(t *testing.T) {
		mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
		th.store.EXPECT().
			GetMembersForBoard(boardID2).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
		require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
	})

	t.Run("should find that only one user is connected to team 2 and board 2 if there is only one membership with both connected", func(t *testing.T) {
		mockedMembers := []*model.BoardMember{{UserID: userID1}}
		th.store.EXPECT().
			GetMembersForBoard(boardID2).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})

	t.Run("should find only one if the other is inactive", func(t *testing.T) {
		th.pa.OnWebSocketDisconnect(webConnID3, userID2)
		defer th.pa.OnWebSocketConnect(webConnID3, userID2)

		mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
		th.store.EXPECT().
			GetMembersForBoard(boardID2).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})

	t.Run("should include a user that is not present if it's ensured", func(t *testing.T) {
		userID3 := mmModel.NewId()
		mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
		th.store.EXPECT().
			GetMembersForBoard(boardID2).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(true).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2, userID3)
		require.ElementsMatch(t, []string{userID1, userID2, userID3}, userIDs)
	})

	t.Run("should not include a user that, although present, has no team access anymore", func(t *testing.T) {
		mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
		th.store.EXPECT().
			GetMembersForBoard(boardID2).
			Return(mockedMembers, nil).
			Times(1)

		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID1, teamID2).
			Return(true).
			Times(1)

		// userID2 has no team access
		th.auth.EXPECT().
			DoesUserHaveTeamAccess(userID2, teamID2).
			Return(false).
			Times(1)

		userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
		require.ElementsMatch(t, []string{userID1}, userIDs)
	})
}

func TestParallelSubscriptionsOnMultipleConnections(t *testing.T) {
	th := SetupTestHelper(t)

	teamID1 := mmModel.NewId()
	teamID2 := mmModel.NewId()
	teamID3 := mmModel.NewId()
	teamID4 := mmModel.NewId()

	userID := mmModel.NewId()
	webConnID1 := mmModel.NewId()
	webConnID2 := mmModel.NewId()

	th.pa.OnWebSocketConnect(webConnID1, userID)
	pac1, ok := th.pa.GetListenerByWebConnID(webConnID1)
	require.True(t, ok)

	th.pa.OnWebSocketConnect(webConnID2, userID)
	pac2, ok := th.pa.GetListenerByWebConnID(webConnID2)
	require.True(t, ok)

	wg := new(sync.WaitGroup)
	wg.Add(4)

	go func(wg *sync.WaitGroup) {
		th.SubscribeWebConnToTeam(webConnID1, userID, teamID1)
		require.True(t, pac1.isSubscribedToTeam(teamID1))

		th.SubscribeWebConnToTeam(webConnID2, userID, teamID1)
		require.True(t, pac2.isSubscribedToTeam(teamID1))

		th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID1)
		require.False(t, pac1.isSubscribedToTeam(teamID1))

		th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID1)
		require.False(t, pac2.isSubscribedToTeam(teamID1))

		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.SubscribeWebConnToTeam(webConnID1, userID, teamID2)
		require.True(t, pac1.isSubscribedToTeam(teamID2))

		th.SubscribeWebConnToTeam(webConnID2, userID, teamID2)
		require.True(t, pac2.isSubscribedToTeam(teamID2))

		th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID2)
		require.False(t, pac1.isSubscribedToTeam(teamID2))

		th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID2)
		require.False(t, pac2.isSubscribedToTeam(teamID2))

		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.SubscribeWebConnToTeam(webConnID1, userID, teamID3)
		require.True(t, pac1.isSubscribedToTeam(teamID3))

		th.SubscribeWebConnToTeam(webConnID2, userID, teamID3)
		require.True(t, pac2.isSubscribedToTeam(teamID3))

		th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID3)
		require.False(t, pac1.isSubscribedToTeam(teamID3))

		th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID3)
		require.False(t, pac2.isSubscribedToTeam(teamID3))

		wg.Done()
	}(wg)

	go func(wg *sync.WaitGroup) {
		th.SubscribeWebConnToTeam(webConnID1, userID, teamID4)
		require.True(t, pac1.isSubscribedToTeam(teamID4))

		th.SubscribeWebConnToTeam(webConnID2, userID, teamID4)
		require.True(t, pac2.isSubscribedToTeam(teamID4))

		th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID4)
		require.False(t, pac1.isSubscribedToTeam(teamID4))

		th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID4)
		require.False(t, pac2.isSubscribedToTeam(teamID4))

		wg.Done()
	}(wg)

	wg.Wait()
}
