// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later

package actions

import (
	"testing"

	actions_model "forgejo.org/models/actions"
	issues_model "forgejo.org/models/issues"
	"forgejo.org/models/unittest"
	user_model "forgejo.org/models/user"
	actions_module "forgejo.org/modules/actions"
	webhook_module "forgejo.org/modules/webhook"

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

func TestActionsTrust_ChangeStatus(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	repoID := int64(10)
	pullRequestPosterID := int64(30)

	runDone := &actions_model.ActionRun{
		RepoID:              repoID,
		PullRequestPosterID: pullRequestPosterID,
		Status:              actions_model.StatusSuccess,
	}
	require.NoError(t, actions_model.InsertRun(t.Context(), runDone, nil))

	runNotByPoster := &actions_model.ActionRun{
		RepoID:              repoID,
		PullRequestPosterID: 43243,
		Status:              actions_model.StatusRunning,
	}
	require.NoError(t, actions_model.InsertRun(t.Context(), runNotByPoster, nil))

	runNotInTheSameRepository := &actions_model.ActionRun{
		RepoID:              5,
		PullRequestPosterID: pullRequestPosterID,
		Status:              actions_model.StatusSuccess,
	}
	require.NoError(t, actions_model.InsertRun(t.Context(), runNotInTheSameRepository, nil))

	t.Run("RevokeTrust", func(t *testing.T) {
		singleWorkflows, err := actions_module.JobParser([]byte(`
jobs:
  job:
    runs-on: docker
    steps:
      - run: echo OK
`))
		require.NoError(t, err)
		require.Len(t, singleWorkflows, 1)
		runNotDone := &actions_model.ActionRun{
			TriggerUserID:       2,
			RepoID:              repoID,
			Status:              actions_model.StatusWaiting,
			PullRequestPosterID: pullRequestPosterID,
		}
		require.NoError(t, actions_model.InsertRun(t.Context(), runNotDone, singleWorkflows))
		require.NoError(t, actions_model.InsertActionUser(t.Context(), &actions_model.ActionUser{
			UserID:                  pullRequestPosterID,
			RepoID:                  repoID,
			TrustedWithPullRequests: true,
		}))

		previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
		_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), pullRequestPosterID, repoID)
		require.NoError(t, err)

		require.NoError(t, RevokeTrust(t.Context(), repoID, pullRequestPosterID))

		_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), pullRequestPosterID, repoID)
		assert.True(t, actions_model.IsErrUserNotExist(err))
		currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
		assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotDone.ID})
		assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
	})

	createPullRequestRun := func(t *testing.T, pullRequestID, repoID int64) *actions_model.ActionRun {
		t.Helper()
		singleWorkflows, err := actions_module.JobParser([]byte(`
jobs:
  job:
    runs-on: docker
    steps:
      - run: echo OK
`))
		require.NoError(t, err)
		require.Len(t, singleWorkflows, 1)
		runNotApproved := &actions_model.ActionRun{
			TriggerUserID:       2,
			RepoID:              repoID,
			Status:              actions_model.StatusWaiting,
			NeedApproval:        true,
			PullRequestID:       pullRequestID,
			PullRequestPosterID: pullRequestPosterID,
		}
		require.NoError(t, actions_model.InsertRun(t.Context(), runNotApproved, singleWorkflows))
		return runNotApproved
	}

	t.Run("PullRequestCancel", func(t *testing.T) {
		pullRequestID := int64(485)
		runNotApproved := createPullRequestRun(t, pullRequestID, repoID)

		previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})

		require.NoError(t, pullRequestCancel(t.Context(), repoID, pullRequestID))

		currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
		assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
		assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
	})

	t.Run("UpdateTrustedWithPullRequest deny", func(t *testing.T) {
		pullRequestID := int64(485)
		runNotApproved := createPullRequestRun(t, pullRequestID, repoID)

		previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})

		require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), 0, &issues_model.PullRequest{
			ID: pullRequestID,
			Issue: &issues_model.Issue{
				RepoID: repoID,
			},
		}, UserTrustDenied))

		currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
		assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
		assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
	})

	t.Run("PullRequestApprove", func(t *testing.T) {
		pullRequestID := int64(534)
		runNotApproved := createPullRequestRun(t, pullRequestID, repoID)

		previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})

		doerID := int64(84322)
		require.NoError(t, pullRequestApprove(t.Context(), doerID, repoID, pullRequestID))

		currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
		assert.Equal(t, previousWaitingCount+1, currentWaitingCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
		assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
		assert.Equal(t, doerID, run.ApprovedBy)
		assert.False(t, run.NeedApproval)
	})

	t.Run("UpdateTrustedWithPullRequest once", func(t *testing.T) {
		pullRequestID := int64(534)
		runNotApproved := createPullRequestRun(t, pullRequestID, repoID)

		previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})

		doerID := int64(84322)
		require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
			ID: pullRequestID,
			Issue: &issues_model.Issue{
				RepoID: repoID,
			},
		}, UserTrustedOnce))

		currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
		assert.Equal(t, previousWaitingCount+1, currentWaitingCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
		assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
		assert.Equal(t, doerID, run.ApprovedBy)
		assert.False(t, run.NeedApproval)
	})

	t.Run("UpdateTrustedWithPullRequest always", func(t *testing.T) {
		pullRequestIDs := []int64{534, 645}
		var runsNotApproved []*actions_model.ActionRun
		for _, pullRequestID := range pullRequestIDs {
			runsNotApproved = append(runsNotApproved, createPullRequestRun(t, pullRequestID, repoID))
		}

		previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})

		doerID := int64(84322)
		require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
			ID: pullRequestIDs[0],
			Issue: &issues_model.Issue{
				RepoID:   repoID,
				PosterID: pullRequestPosterID,
			},
		}, UserAlwaysTrusted))

		currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
		assert.Equal(t, previousWaitingCount+len(pullRequestIDs), currentWaitingCount)

		for _, run := range runsNotApproved {
			run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
			assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
			assert.Equal(t, doerID, run.ApprovedBy)
			assert.False(t, run.NeedApproval)
		}
	})

	t.Run("UpdateTrustedWithPullRequest revoke", func(t *testing.T) {
		pullRequestIDs := []int64{748, 953}
		var runsNotApproved []*actions_model.ActionRun
		for _, pullRequestID := range pullRequestIDs {
			runsNotApproved = append(runsNotApproved, createPullRequestRun(t, pullRequestID, repoID))
		}

		doerID := int64(84322)
		require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
			ID: pullRequestIDs[0],
			Issue: &issues_model.Issue{
				RepoID:   repoID,
				PosterID: pullRequestPosterID,
			},
		}, UserTrustRevoked))

		for _, run := range runsNotApproved {
			run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
			assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
			assert.False(t, run.NeedApproval)
		}
	})

	t.Run("cleanupPullRequestUnapprovedRuns", func(t *testing.T) {
		pullRequestID := int64(534)
		runNotApproved := createPullRequestRun(t, pullRequestID, repoID)

		previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})

		require.NoError(t, cleanupPullRequestUnapprovedRuns(t.Context(), repoID, pullRequestID))

		currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
		assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
		assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
	})
}

func TestActionsTrust_GetPullRequestUserIsTrustedWithActions(t *testing.T) {
	defer unittest.OverrideFixtures("services/actions/TestActionsTrust_GetPullRequestUserIsTrustedWithActions")()
	require.NoError(t, unittest.PrepareTestDatabase())

	t.Run("implicitly trusted because the pull request is not from a fork", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2000})
		trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.False(t, pr.IsForkPullRequest())
		assert.Equal(t, UserIsImplicitlyTrustedWithActions, trust)
	})

	t.Run("implicitly trusted on a forked pull request when the poster is admin", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
		trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.True(t, pr.IsForkPullRequest())
		require.True(t, pr.Issue.Poster.IsAdmin)
		assert.Equal(t, UserIsImplicitlyTrustedWithActions, trust)
	})

	t.Run("explicitly trusted on a forked pull request when the poster was permanently approved", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1000})
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // regular user
		trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.True(t, pr.IsForkPullRequest())
		_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), user.ID, pr.Issue.RepoID)
		require.NoError(t, err)
		assert.Equal(t, UserIsExplicitlyTrustedWithActions, trust)
	})

	t.Run("not trusted because on a forked pull request when the user has has no privileges", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // regular user
		trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		assert.Equal(t, user.ID, pr.Issue.PosterID)
		require.True(t, pr.IsForkPullRequest())
		assert.Equal(t, UserIsNotTrustedWithActions, trust)
	})

	t.Run("not trusted on a forked pull request because the user is restricted", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5000})
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) // restricted user
		trust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, user)
		require.NoError(t, err)
		assert.Equal(t, user.ID, pr.Issue.PosterID)
		require.True(t, pr.IsForkPullRequest())
		_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), user.ID, pr.Issue.RepoID)
		require.NoError(t, err)
		require.True(t, user.IsRestricted)
		assert.Equal(t, UserIsNotTrustedWithActions, trust)
	})

	t.Run("approval not needed because the pr is not from a fork", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2000})
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequest)
		require.NoError(t, err)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("approval not needed because the event is known to run out of the default branch", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequestComment)
		require.NoError(t, err)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("approval not needed because it is not a pr", func(t *testing.T) {
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), nil, nil, webhook_module.HookEventPullRequestComment)
		require.NoError(t, err)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("approval not needed for a forked pr because the poster is trusted", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequestSync)
		require.NoError(t, err)
		require.True(t, pr.Issue.Poster.IsAdmin)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("approval needed for a forked pr because the poster and the doer are not trusted", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // regular user
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestSync)
		require.NoError(t, err)
		posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
		doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
		require.NoError(t, err)
		require.Equal(t, UserIsNotTrustedWithActions, doerTrust)
		assert.Equal(t, actions_model.NeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("approval not needed for a forked pr because the doer is trusted and runs from the base", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // admin
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestLabel)
		require.NoError(t, err)
		posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
		doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
		require.NoError(t, err)
		require.Equal(t, UserIsImplicitlyTrustedWithActions, doerTrust)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useBaseCommit, useCommit)
	})

	t.Run("approval not needed for a forked pr because the doer is trusted and pushed new commits", func(t *testing.T) {
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // admin
		useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestSync)
		require.NoError(t, err)
		posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
		require.NoError(t, err)
		require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
		doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
		require.NoError(t, err)
		require.Equal(t, UserIsImplicitlyTrustedWithActions, doerTrust)
		assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
		assert.EqualValues(t, useHeadCommit, useCommit)
	})

	t.Run("run for a pull request is set with info related to trust", func(t *testing.T) {
		run := &actions_model.ActionRun{
			IsForkPullRequest: true,
		}
		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5000})
		needApproval := actions_model.NeedApproval
		require.NoError(t, setRunTrustForPullRequest(t.Context(), run, nil, needApproval))
		require.NoError(t, setRunTrustForPullRequest(t.Context(), run, pr, needApproval))
		assert.True(t, run.NeedApproval)
		assert.True(t, run.IsForkPullRequest)
		assert.Equal(t, pr.Issue.PosterID, run.PullRequestPosterID)
		assert.Equal(t, pr.ID, run.PullRequestID)
	})
}
