// Copyright 2026 The Forgejo Authors
// SPDX-License-Identifier: GPL-3.0-or-later

package integration

import (
	"fmt"
	"net/http"
	"strings"
	"testing"

	"forgejo.org/models/db"
	"forgejo.org/models/organization"
	"forgejo.org/models/unittest"
	user_model "forgejo.org/models/user"
	"forgejo.org/tests"

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

func TestOrgMembersPage(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	testPage := "/org/org3/members"

	t.Run("Guest PoV", func(t *testing.T) {
		defer tests.PrintCurrentTest(t)()

		doc := NewHTMLParser(t, MakeRequest(t, NewRequest(t, "GET", testPage), http.StatusOK).Body)
		/* No interactive buttons - though such evaluation is easy to break in rename */
		assert.Equal(t, 0, doc.Find(".members .list .link-action").Length())
		assert.Equal(t, 0, doc.Find(".members .list .delete-button").Length())
		/* Guests cannot add members to organizations */
		doc.AssertElement(t, "#add-org-member-button", false)
	})

	t.Run("Member PoV", func(t *testing.T) {
		defer tests.PrintCurrentTest(t)()

		session := loginUser(t, "user4") // user4 is a member of org3
		doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", testPage), http.StatusOK).Body)
		/* Interactive buttons are only available for own entry in the list */
		assert.Equal(t, 1, doc.Find(".members .list .link-action").Length())
		assert.Equal(t, 1, doc.Find(".members .list .delete-button").Length())
		/* Adding new members is not possible, as the member isn't an owner */
		doc.AssertElement(t, "#add-org-member-button", false)
	})

	t.Run("Owner PoV", func(t *testing.T) {
		defer tests.PrintCurrentTest(t)()

		session := loginUser(t, "user2") // user2 owns org3
		doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", testPage), http.StatusOK).Body)
		/* Interactive buttons are available for all entries in the list (> 2) */
		assert.Less(t, 2, doc.Find(".members .list .link-action").Length())
		assert.Less(t, 2, doc.Find(".members .list .delete-button").Length())
		/* Adding new members is possible */
		doc.AssertElement(t, "#add-org-member-button", true)
	})
}

func TestOrgAddMember(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
	team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})

	isMember, err := organization.IsTeamMember(db.DefaultContext, team1.OrgID, team1.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isMember)
	isMember, err = organization.IsTeamMember(db.DefaultContext, team2.OrgID, team2.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isMember)

	session := loginUser(t, "user2")

	teamURL := fmt.Sprintf("/org/%s/members", org.Name)
	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
		"uid":    "2",
		"uname":  user.LoginName,
		"team_1": "on",
		"team_2": "on",
	})
	resp := session.MakeRequest(t, req, http.StatusSeeOther)
	assert.Equal(t, teamURL, resp.Header().Get("Location"))

	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
	isMember, err = organization.IsTeamMember(db.DefaultContext, team1.OrgID, team1.ID, user.ID)
	require.NoError(t, err)
	assert.True(t, isMember)
	isMember, err = organization.IsTeamMember(db.DefaultContext, team2.OrgID, team2.ID, user.ID)
	require.NoError(t, err)
	assert.True(t, isMember)
}

func TestOrgAddMemberToNoTeam(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})

	isOrgMember, err := org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.False(t, isOrgMember)

	session := loginUser(t, "user2")

	teamURL := fmt.Sprintf("/org/%s/members", org.Name)
	resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
		"uid":   "2",
		"uname": user.LoginName,
	}), http.StatusSeeOther)
	assert.Equal(t, teamURL, resp.Header().Get("Location"))

	doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", teamURL), http.StatusOK).Body)
	assert.Contains(t, strings.TrimSpace(doc.Find(".flash-error").Text()), "Organization members must belong to at least one team.")

	isOrgMember, err = org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.False(t, isOrgMember)
}

func TestOrgAddMemberToForeignTeam(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
	foreignTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})

	isOrgMember, err := org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.False(t, isOrgMember)

	isForeignTeamMember, err := organization.IsTeamMember(db.DefaultContext, foreignTeam.OrgID, foreignTeam.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isForeignTeamMember)

	session := loginUser(t, "user2")

	teamURL := fmt.Sprintf("/org/%s/members", org.Name)
	// This test scenario is here for security purposes as the UI will not offer adding the user to
	// a foreign team in the first place (but a malicious request could be performed in other ways).
	resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
		"uid":    "2",
		"uname":  user.LoginName,
		"team_5": "on",
	}), http.StatusSeeOther)
	assert.Equal(t, teamURL, resp.Header().Get("Location"))

	doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", teamURL), http.StatusOK).Body)
	// This error message isn't specific to this "exploit" attempt, but shows that no action was taken.
	assert.Contains(t, strings.TrimSpace(doc.Find(".flash-error").Text()), "Organization members must belong to at least one team.")

	isOrgMember, err = org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.False(t, isOrgMember)

	isForeignTeamMember, err = organization.IsTeamMember(db.DefaultContext, foreignTeam.OrgID, foreignTeam.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isForeignTeamMember)
}

func TestOrgAddMemberWithoutProperRights(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})

	isOrgMember, err := org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.False(t, isOrgMember)

	session := loginUser(t, user.Name) // user5 is not a member of this org, so it cannot add anyone to it

	teamURL := fmt.Sprintf("/org/%s/members", org.Name)
	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
		"uid":    "5",
		"uname":  user.LoginName,
		"team_2": "on",
	})
	session.MakeRequest(t, req, http.StatusNotFound)
}

func TestOrgAddExistingMemberFails(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})

	members, _, err := organization.FindOrgMembers(db.DefaultContext, &organization.FindOrgMembersOpts{
		OrgID: org.ID,
	})
	require.NoError(t, err)
	assert.Len(t, members, 2)
	isOrgMember, err := org.IsOrgMember(db.DefaultContext, user.ID)
	require.NoError(t, err)
	assert.True(t, isOrgMember)
	isTeamMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isTeamMember)

	session := loginUser(t, "user2")

	teamURL := fmt.Sprintf("/org/%s/members", org.Name)
	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
		"uid":    "2",
		"uname":  user.LoginName,
		"team_2": "on",
	})
	resp := session.MakeRequest(t, req, http.StatusSeeOther)
	assert.Equal(t, teamURL, resp.Header().Get("Location"))
	doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", teamURL), http.StatusOK).Body)
	assert.Contains(t, strings.TrimSpace(doc.Find(".flash-error").Text()), "This user is already a member of the organization.")

	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
	isTeamMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
	require.NoError(t, err)
	assert.False(t, isTeamMember)
}
