// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
	"net/url"
	"strconv"
	"strings"
	"testing"

	actions_model "code.gitea.io/gitea/models/actions"
	auth_model "code.gitea.io/gitea/models/auth"
	git_model "code.gitea.io/gitea/models/git"
	issues_model "code.gitea.io/gitea/models/issues"
	repo_model "code.gitea.io/gitea/models/repo"
	unit_model "code.gitea.io/gitea/models/unit"
	"code.gitea.io/gitea/models/unittest"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/migration"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/util"
	mirror_service "code.gitea.io/gitea/services/mirror"
	repo_service "code.gitea.io/gitea/services/repository"
	files_service "code.gitea.io/gitea/services/repository/files"

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

func TestScheduleUpdate(t *testing.T) {
	t.Run("Push", testScheduleUpdatePush)
	t.Run("PullMerge", testScheduleUpdatePullMerge)
	t.Run("DisableAndEnableActionsUnit", testScheduleUpdateDisableAndEnableActionsUnit)
	t.Run("ArchiveAndUnarchive", testScheduleUpdateArchiveAndUnarchive)
	t.Run("MirrorSync", testScheduleUpdateMirrorSync)
}

func testScheduleUpdatePush(t *testing.T) {
	doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
		newCron := "30 5 * * 1,3"
		pushScheduleChange(t, u, repo, newCron)
		branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
		assert.NoError(t, err)
		return branch.CommitID, newCron
	})
}

func testScheduleUpdatePullMerge(t *testing.T) {
	newBranchName := "feat1"
	workflowTreePath := ".gitea/workflows/actions-schedule.yml"
	workflowContent := `name: actions-schedule
on:
  schedule:
    - cron:  '@every 2m' # update to 2m
jobs:
  job:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'schedule workflow'
`

	mergeStyles := []repo_model.MergeStyle{
		repo_model.MergeStyleMerge,
		repo_model.MergeStyleRebase,
		repo_model.MergeStyleRebaseMerge,
		repo_model.MergeStyleSquash,
		repo_model.MergeStyleFastForwardOnly,
	}

	for _, mergeStyle := range mergeStyles {
		t.Run(string(mergeStyle), func(t *testing.T) {
			doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
				// update workflow file
				_, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
					NewBranch: newBranchName,
					Files: []*files_service.ChangeRepoFile{
						{
							Operation:     "update",
							TreePath:      workflowTreePath,
							ContentReader: strings.NewReader(workflowContent),
						},
					},
					Message: "update workflow schedule",
				})
				assert.NoError(t, err)

				// create pull request
				apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
				assert.NoError(t, err)

				// merge pull request
				testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false)

				pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
				return pull.MergedCommitID, "@every 2m"
			})
		})
	}

	t.Run(string(repo_model.MergeStyleManuallyMerged), func(t *testing.T) {
		doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
			// enable manual-merge
			doAPIEditRepository(testContext, &api.EditRepoOption{
				HasPullRequests:  util.ToPointer(true),
				AllowManualMerge: util.ToPointer(true),
			})(t)

			// update workflow file
			fileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
				NewBranch: newBranchName,
				Files: []*files_service.ChangeRepoFile{
					{
						Operation:     "update",
						TreePath:      workflowTreePath,
						ContentReader: strings.NewReader(workflowContent),
					},
				},
				Message: "update workflow schedule",
			})
			assert.NoError(t, err)

			// merge and push
			dstPath := t.TempDir()
			u.Path = repo.FullName() + ".git"
			u.User = url.UserPassword(repo.OwnerName, userPassword)
			doGitClone(dstPath, u)(t)
			doGitMerge(dstPath, "origin/"+newBranchName)(t)
			doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)

			// create pull request
			apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
			assert.NoError(t, err)

			// merge pull request manually
			doAPIManuallyMergePullRequest(testContext, repo.OwnerName, repo.Name, fileResp.Commit.SHA, apiPull.Index)(t)

			pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
			assert.Equal(t, issues_model.PullRequestStatusManuallyMerged, pull.Status)
			return pull.MergedCommitID, "@every 2m"
		})
	})
}

func testScheduleUpdateMirrorSync(t *testing.T) {
	doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
		// create mirror repo
		opts := migration.MigrateOptions{
			RepoName:    "actions-schedule-mirror",
			Description: "Test mirror for actions-schedule",
			Private:     false,
			Mirror:      true,
			CloneAddr:   repo.CloneLinkGeneral(t.Context()).HTTPS,
		}
		mirrorRepo, err := repo_service.CreateRepositoryDirectly(t.Context(), user, user, repo_service.CreateRepoOptions{
			Name:          opts.RepoName,
			Description:   opts.Description,
			IsPrivate:     opts.Private,
			IsMirror:      opts.Mirror,
			DefaultBranch: repo.DefaultBranch,
			Status:        repo_model.RepositoryBeingMigrated,
		}, false)
		assert.NoError(t, err)
		assert.True(t, mirrorRepo.IsMirror)
		mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil)
		assert.NoError(t, err)
		mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository)

		// enable actions unit for mirror repo
		assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
		doAPIEditRepository(mirrorContext, &api.EditRepoOption{
			HasActions: util.ToPointer(true),
		})(t)
		actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
		scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
		assert.Equal(t, "@every 1m", scheduleSpec.Spec)

		// update remote repo
		newCron := "30 5,17 * * 2,4"
		pushScheduleChange(t, u, repo, newCron)
		repoDefaultBranch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
		assert.NoError(t, err)

		// sync
		ok := mirror_service.SyncPullMirror(t.Context(), mirrorRepo.ID)
		assert.True(t, ok)
		mirrorRepoDefaultBranch, err := git_model.GetBranch(t.Context(), mirrorRepo.ID, mirrorRepo.DefaultBranch)
		assert.NoError(t, err)
		assert.Equal(t, repoDefaultBranch.CommitID, mirrorRepoDefaultBranch.CommitID)

		// check updated schedule
		actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
		scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
		assert.Equal(t, newCron, scheduleSpec.Spec)

		return repoDefaultBranch.CommitID, newCron
	})
}

func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
	doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
		doAPIEditRepository(testContext, &api.EditRepoOption{
			Archived: util.ToPointer(true),
		})(t)
		assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
		doAPIEditRepository(testContext, &api.EditRepoOption{
			Archived: util.ToPointer(false),
		})(t)
		branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
		assert.NoError(t, err)
		return branch.CommitID, "@every 1m"
	})
}

func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
	doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
		doAPIEditRepository(testContext, &api.EditRepoOption{
			HasActions: util.ToPointer(false),
		})(t)
		assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
		doAPIEditRepository(testContext, &api.EditRepoOption{
			HasActions: util.ToPointer(true),
		})(t)
		branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
		assert.NoError(t, err)
		return branch.CommitID, "@every 1m"
	})
}

type scheduleUpdateTrigger func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string)

func doTestScheduleUpdate(t *testing.T, updateTrigger scheduleUpdateTrigger) {
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
		session := loginUser(t, user2.Name)
		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)

		apiRepo := createActionsTestRepo(t, token, "actions-schedule", false)
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
		assert.NoError(t, repo.LoadAttributes(t.Context()))
		httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
		defer doAPIDeleteRepository(httpContext)(t)

		wfTreePath := ".gitea/workflows/actions-schedule.yml"
		wfFileContent := `name: actions-schedule
on:
  schedule:
    - cron:  '@every 1m'
jobs:
  job:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'schedule workflow'
`

		opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
		apiFileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts1)

		actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: apiFileResp.Commit.SHA})
		scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
		assert.Equal(t, "@every 1m", scheduleSpec.Spec)

		commitID, expectedSpec := updateTrigger(t, u, httpContext, user2, repo)

		actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: commitID})
		scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
		assert.Equal(t, expectedSpec, scheduleSpec.Spec)
	})
}

func pushScheduleChange(t *testing.T, u *url.URL, repo *repo_model.Repository, newCron string) {
	workflowTreePath := ".gitea/workflows/actions-schedule.yml"
	workflowContent := `name: actions-schedule
on:
  schedule:
    - cron:  '` + newCron + `'
jobs:
  job:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'schedule workflow'
`

	dstPath := t.TempDir()
	u.Path = repo.FullName() + ".git"
	u.User = url.UserPassword(repo.OwnerName, userPassword)
	doGitClone(dstPath, u)(t)
	doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
		LocalRepoPath:   dstPath,
		CheckoutBranch:  repo.DefaultBranch,
		TreeFilePath:    workflowTreePath,
		TreeFileContent: workflowContent,
	})(t)
	doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
}
