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

package integration

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

	runnerv1 "gitea.dev/actions-proto-go/runner/v1"
	actions_model "gitea.dev/models/actions"
	auth_model "gitea.dev/models/auth"
	"gitea.dev/models/db"
	repo_model "gitea.dev/models/repo"
	"gitea.dev/models/unittest"
	user_model "gitea.dev/models/user"
	"gitea.dev/modules/actions/jobparser"
	"gitea.dev/modules/setting"
	api "gitea.dev/modules/structs"
	"gitea.dev/modules/test"
	"gitea.dev/modules/timeutil"
	actions_web "gitea.dev/routers/web/repo/actions"

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

func TestActionsRerun(t *testing.T) {
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
		userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
		sessionAdmin := loginUser(t, userAdmin.Name)

		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-rerun", false)
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
		httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
		defer doAPIDeleteRepository(httpContext)(t)

		runner := newMockRunner()
		runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)

		wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
		wfFileContent := `name: actions-rerun-workflow-1
on:
  push:
    paths:
      - '.gitea/workflows/actions-rerun-workflow-1.yml'
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'job1'
  job2:
    runs-on: ubuntu-latest
    needs: [job1]
    steps:
      - run: echo 'job2'
`

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

		// fetch and exec job1
		job1Task := runner.fetchTask(t)
		assert.Equal(t, "1", job1Task.Context.GetFields()["run_attempt"].GetStringValue())
		_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
		runner.execTask(t, job1Task, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		// RERUN-FAILURE: the run is not done
		req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
		session.MakeRequest(t, req, http.StatusBadRequest)
		// fetch and exec job2
		job2Task := runner.fetchTask(t)
		_, job2, _ := getTaskAndJobAndRunByTaskID(t, job2Task.Id)
		runner.execTask(t, job2Task, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		assert.EqualValues(t, 1, getRunLatestAttemptNum(t, run.ID))

		// RERUN-1: rerun the run
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
		sessionAdmin.MakeRequest(t, req, http.StatusOK) // triggered by admin user
		// fetch and exec job1
		job1TaskR1 := runner.fetchTask(t)
		assert.Equal(t, "2", job1TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
		_, job1R1, _ := getTaskAndJobAndRunByTaskID(t, job1TaskR1.Id)
		assert.Equal(t, job1.AttemptJobID, job1R1.AttemptJobID)
		runner.execTask(t, job1TaskR1, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		// fetch and exec job2
		job2TaskR1 := runner.fetchTask(t)
		assert.Equal(t, "2", job2TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
		_, job2R1, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.Id)
		assert.Equal(t, job2.AttemptJobID, job2R1.AttemptJobID)
		runner.execTask(t, job2TaskR1, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		assert.EqualValues(t, 2, getRunLatestAttemptNum(t, run.ID))

		// RERUN-2: rerun job1
		job1 = getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job1.ID))
		session.MakeRequest(t, req, http.StatusOK)
		// job2 needs job1, so rerunning job1 will also rerun job2
		// fetch and exec job1
		job1TaskR2 := runner.fetchTask(t)
		assert.Equal(t, "3", job1TaskR2.Context.GetFields()["run_attempt"].GetStringValue())
		runner.execTask(t, job1TaskR2, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		// fetch and exec job2
		job2TaskR2 := runner.fetchTask(t)
		assert.Equal(t, "3", job2TaskR2.Context.GetFields()["run_attempt"].GetStringValue())
		runner.execTask(t, job2TaskR2, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		assert.EqualValues(t, 3, getRunLatestAttemptNum(t, run.ID))

		// RERUN-3: rerun job2
		job2 = getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
		session.MakeRequest(t, req, http.StatusOK)
		// only job2 will rerun
		// fetch and exec job2
		job2TaskR3 := runner.fetchTask(t)
		assert.Equal(t, "4", job2TaskR3.Context.GetFields()["run_attempt"].GetStringValue())
		runner.execTask(t, job2TaskR3, &mockTaskOutcome{
			result: runnerv1.Result_RESULT_SUCCESS,
		})
		runner.fetchNoTask(t)
		assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))

		runLatestAttempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
		job2LatestAttempt := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
		assert.Equal(t, runLatestAttempt.LatestAttemptID, job2LatestAttempt.RunAttemptID)

		t.Run("AttemptAPI", func(t *testing.T) {
			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/2", user2.Name, repo.Name, run.ID)).
				AddTokenAuth(token)
			attemptResp := MakeRequest(t, req, http.StatusOK)
			apiAttempt := DecodeJSON(t, attemptResp, &api.ActionWorkflowRun{})
			assert.Equal(t, run.ID, apiAttempt.ID)
			assert.EqualValues(t, 2, apiAttempt.RunAttempt)
			assert.Equal(t, "completed", apiAttempt.Status)
			assert.Equal(t, "success", apiAttempt.Conclusion)
			assert.NotNil(t, apiAttempt.PreviousAttemptURL)
			assert.True(t, strings.HasSuffix(*apiAttempt.PreviousAttemptURL, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, run.ID)))
			assert.Equal(t, user2.Name, apiAttempt.Actor.UserName)
			assert.Equal(t, userAdmin.Name, apiAttempt.TriggerActor.UserName)

			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/2/jobs", user2.Name, repo.Name, run.ID)).
				AddTokenAuth(token)
			attemptJobsResp := MakeRequest(t, req, http.StatusOK)
			apiAttemptJobs := DecodeJSON(t, attemptJobsResp, &api.ActionWorkflowJobsResponse{})
			assert.Len(t, apiAttemptJobs.Entries, 2)
			assert.ElementsMatch(t, []int64{job1R1.ID, job2R1.ID}, []int64{apiAttemptJobs.Entries[0].ID, apiAttemptJobs.Entries[1].ID})
		})

		t.Run("MaxRerunAttempts", func(t *testing.T) {
			// The run has 4 attempts after the previous reruns. Lower the cap to 4 to hit the limit.
			defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(4))()

			req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
			resp := session.MakeRequest(t, req, http.StatusBadRequest)
			assert.Contains(t, resp.Body.String(), "workflow run has reached the maximum")
			assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))

			// Raising the cap lets rerun proceed again.
			defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(5))()

			req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
			session.MakeRequest(t, req, http.StatusOK)
			// fetch and exec job1
			job1TaskR4 := runner.fetchTask(t)
			assert.Equal(t, "5", job1TaskR4.Context.GetFields()["run_attempt"].GetStringValue())
			runner.execTask(t, job1TaskR4, &mockTaskOutcome{
				result: runnerv1.Result_RESULT_SUCCESS,
			})
			job2TaskR4 := runner.fetchTask(t)
			assert.Equal(t, "5", job2TaskR4.Context.GetFields()["run_attempt"].GetStringValue())
			runner.execTask(t, job2TaskR4, &mockTaskOutcome{
				result: runnerv1.Result_RESULT_SUCCESS,
			})
			assert.EqualValues(t, 5, getRunLatestAttemptNum(t, run.ID))
		})
	})
}

func TestActionsRerunLegacyNoAttemptRun(t *testing.T) {
	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-rerun-legacy", false)
		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
		httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
		defer doAPIDeleteRepository(httpContext)(t)
		runner := newMockRunner()
		runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)

		wfTreePath := ".gitea/workflows/actions-rerun-legacy.yml"
		wfFileContent := `name: actions-rerun-legacy
on:
  workflow_dispatch:
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'job1'
  job2:
    runs-on: ubuntu-latest
    needs: [job1]
    steps:
      - run: echo 'job2'
`

		opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
		fileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
		require.NotNil(t, fileResp)

		// Start preparing legacy data

		payloads := mustParseSingleWorkflowPayloads(t, wfFileContent)
		now := timeutil.TimeStamp(time.Now().Unix())
		started := now - 20
		stopped := now - 10

		legacyRun := &actions_model.ActionRun{
			Title:         "legacy rerun test",
			RepoID:        repo.ID,
			OwnerID:       repo.OwnerID,
			WorkflowID:    "actions-rerun-legacy.yml",
			Index:         1,
			TriggerUserID: user2.ID,
			Ref:           "refs/heads/" + repo.DefaultBranch,
			CommitSHA:     fileResp.Commit.SHA,
			Event:         "workflow_dispatch",
			TriggerEvent:  "workflow_dispatch",
			EventPayload:  "{}",
			Status:        actions_model.StatusSuccess,
			Started:       started,
			Stopped:       stopped,
			Created:       started - 5,
			Updated:       stopped,
		}
		require.NoError(t, db.Insert(t.Context(), legacyRun))
		// xorm does not update "created"-tagged fields via ORM methods; use raw SQL to backfill historical timestamps.
		_, err := db.GetEngine(t.Context()).Exec("UPDATE action_run SET created=?, updated=? WHERE id=?", int64(started-5), int64(stopped), legacyRun.ID)
		require.NoError(t, err)
		legacyRun.Created = started - 5
		legacyRun.Updated = stopped

		legacyJob1 := &actions_model.ActionRunJob{
			RunID:             legacyRun.ID,
			RepoID:            repo.ID,
			OwnerID:           repo.OwnerID,
			CommitSHA:         legacyRun.CommitSHA,
			Name:              payloads["job1"].name,
			Attempt:           1,
			WorkflowPayload:   payloads["job1"].payload,
			JobID:             "job1",
			Needs:             payloads["job1"].needs,
			RunsOn:            payloads["job1"].runsOn,
			Status:            actions_model.StatusSuccess,
			RunAttemptID:      0,
			AttemptJobID:      0,
			Started:           started,
			Stopped:           stopped,
			IsForkPullRequest: false,
		}
		legacyJob2 := &actions_model.ActionRunJob{
			RunID:             legacyRun.ID,
			RepoID:            repo.ID,
			OwnerID:           repo.OwnerID,
			CommitSHA:         legacyRun.CommitSHA,
			Name:              payloads["job2"].name,
			Attempt:           1,
			WorkflowPayload:   payloads["job2"].payload,
			JobID:             "job2",
			Needs:             payloads["job2"].needs,
			RunsOn:            payloads["job2"].runsOn,
			Status:            actions_model.StatusSuccess,
			RunAttemptID:      0,
			AttemptJobID:      0,
			Started:           started,
			Stopped:           stopped,
			IsForkPullRequest: false,
		}
		require.NoError(t, db.Insert(t.Context(), legacyJob1, legacyJob2))

		legacyTask1 := &actions_model.ActionTask{
			JobID:             legacyJob1.ID,
			Attempt:           1,
			Status:            actions_model.StatusSuccess,
			Started:           started,
			Stopped:           stopped,
			RepoID:            repo.ID,
			OwnerID:           repo.OwnerID,
			CommitSHA:         legacyRun.CommitSHA,
			IsForkPullRequest: false,
		}
		legacyTask1.GenerateAndFillToken()
		legacyTask2 := &actions_model.ActionTask{
			JobID:             legacyJob2.ID,
			Attempt:           1,
			Status:            actions_model.StatusSuccess,
			Started:           started,
			Stopped:           stopped,
			RepoID:            repo.ID,
			OwnerID:           repo.OwnerID,
			CommitSHA:         legacyRun.CommitSHA,
			IsForkPullRequest: false,
		}
		legacyTask2.GenerateAndFillToken()
		require.NoError(t, db.Insert(t.Context(), legacyTask1, legacyTask2))

		legacyJob1.TaskID = legacyTask1.ID
		legacyJob2.TaskID = legacyTask2.ID
		_, err = db.GetEngine(t.Context()).ID(legacyJob1.ID).Cols("task_id").Update(legacyJob1)
		require.NoError(t, err)
		_, err = db.GetEngine(t.Context()).ID(legacyJob2.ID).Cols("task_id").Update(legacyJob2)
		require.NoError(t, err)

		legacyArtifact := &actions_model.ActionArtifact{
			RunID:                 legacyRun.ID,
			RunAttemptID:          0,
			RepoID:                repo.ID,
			OwnerID:               repo.OwnerID,
			CommitSHA:             legacyRun.CommitSHA,
			StoragePath:           "artifacts/legacy-artifact.zip",
			FileSize:              123,
			FileCompressedSize:    123,
			ContentEncodingOrType: actions_model.ContentTypeZip,
			ArtifactPath:          "legacy-artifact.zip",
			ArtifactName:          "legacy-artifact",
			Status:                actions_model.ArtifactStatusUploadConfirmed,
			ExpiredUnix:           now + timeutil.Day,
		}
		require.NoError(t, db.Insert(t.Context(), legacyArtifact))

		// Done preparing legacy data

		// assert the web view for the legacy run before rerun
		req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
		legacyResp := session.MakeRequest(t, req, http.StatusOK)
		legacyView := DecodeJSON(t, legacyResp, &actions_web.ViewResponse{})
		// legacy run has no attempt records, so RunAttempt is 0 and Attempts list is empty
		assert.EqualValues(t, 0, legacyView.State.Run.RunAttempt)
		assert.Empty(t, legacyView.State.Run.Attempts)
		assert.Equal(t, "success", legacyView.State.Run.Status)
		assert.True(t, legacyView.State.Run.Done)
		// isLatestAttempt=true, done=true: can rerun but not cancel
		assert.False(t, legacyView.State.Run.CanCancel)
		assert.False(t, legacyView.State.Run.CanApprove)
		assert.True(t, legacyView.State.Run.CanRerun)
		assert.False(t, legacyView.State.Run.CanRerunFailed) // all jobs succeeded
		assert.True(t, legacyView.State.Run.CanDeleteArtifact)
		if assert.Len(t, legacyView.State.Run.Jobs, 2) {
			assert.Equal(t, legacyJob1.ID, legacyView.State.Run.Jobs[0].ID)
			assert.Equal(t, legacyJob2.ID, legacyView.State.Run.Jobs[1].ID)
		}
		if assert.Len(t, legacyView.Artifacts, 1) {
			assert.Equal(t, legacyArtifact.ArtifactName, legacyView.Artifacts[0].Name)
			assert.Equal(t, "completed", legacyView.Artifacts[0].Status)
		}

		// rerun the legacy run
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, legacyRun.ID))
		session.MakeRequest(t, req, http.StatusOK)
		runAfterRerun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
		assert.EqualValues(t, 2, getRunLatestAttemptNum(t, legacyRun.ID))
		jobsAfterRerun, err := actions_model.GetRunJobsByRunAndAttemptID(t.Context(), legacyRun.ID, runAfterRerun.LatestAttemptID)
		require.NoError(t, err)
		require.Len(t, jobsAfterRerun, 2)
		rerunJobsByJobID := map[string]*actions_model.ActionRunJob{}
		for _, job := range jobsAfterRerun {
			rerunJobsByJobID[job.JobID] = job
		}
		require.Contains(t, rerunJobsByJobID, "job1")
		require.Contains(t, rerunJobsByJobID, "job2")
		assert.Equal(t, actions_model.StatusWaiting, rerunJobsByJobID["job1"].Status)
		assert.Equal(t, actions_model.StatusBlocked, rerunJobsByJobID["job2"].Status)

		// fetch job1 rerun task
		job1TaskR1 := runner.fetchTask(t)
		assert.Equal(t, "2", job1TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
		rerunJob1Task, rerunJob1, rerunRun := getTaskAndJobAndRunByTaskID(t, job1TaskR1.Id)
		assert.Equal(t, legacyRun.ID, rerunRun.ID)
		assert.Equal(t, rerunJob1.RunAttemptID, rerunRun.LatestAttemptID)
		runner.execTask(t, job1TaskR1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})

		// fetch job2 rerun task
		job2TaskR1 := runner.fetchTask(t)
		assert.Equal(t, "2", job2TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
		rerunJob2Task, rerunJob2, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.Id)
		runner.execTask(t, job2TaskR1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
		runner.fetchNoTask(t)

		// query the 2 attempts
		runAfterRerun = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
		attempt1, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 1)
		require.NoError(t, err)
		assert.Equal(t, legacyRun.Created, attempt1.Created)
		assert.Equal(t, legacyRun.Started, attempt1.Started)
		assert.Equal(t, legacyRun.Stopped, attempt1.Stopped)
		attempt2, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 2)
		require.NoError(t, err)
		assert.Equal(t, attempt2.ID, runAfterRerun.LatestAttemptID)
		assert.Equal(t, runAfterRerun.Created, attempt1.Created)
		assert.Equal(t, runAfterRerun.Started, attempt2.Started)
		assert.Equal(t, runAfterRerun.Stopped, attempt2.Stopped)

		// assert legacy jobs
		legacyJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob1.ID})
		legacyJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob2.ID})
		assert.Equal(t, attempt1.ID, legacyJob1.RunAttemptID)
		assert.Equal(t, attempt1.ID, legacyJob2.RunAttemptID)
		assert.EqualValues(t, 1, legacyJob1.Attempt)
		assert.EqualValues(t, 1, legacyJob2.Attempt)
		assert.EqualValues(t, 1, legacyJob1.AttemptJobID)
		assert.EqualValues(t, 2, legacyJob2.AttemptJobID)
		legacyTask1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask1.ID})
		legacyTask2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask2.ID})
		assert.EqualValues(t, 1, legacyTask1.Attempt)
		assert.EqualValues(t, 1, legacyTask2.Attempt)

		// assert legacy artifacts
		legacyArtifact = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: legacyArtifact.ID})
		assert.Equal(t, attempt1.ID, legacyArtifact.RunAttemptID)

		// assert jobs of the latest rerun
		rerunJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob1.ID})
		rerunJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob2.ID})
		assert.Equal(t, attempt2.ID, rerunJob1.RunAttemptID)
		assert.Equal(t, attempt2.ID, rerunJob2.RunAttemptID)
		assert.Equal(t, legacyJob1.AttemptJobID, rerunJob1.AttemptJobID)
		assert.Equal(t, legacyJob2.AttemptJobID, rerunJob2.AttemptJobID)
		assert.EqualValues(t, 2, rerunJob1Task.Attempt)
		assert.EqualValues(t, 2, rerunJob2Task.Attempt)

		// assert the web view for the original attempt
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, legacyRun.ID))
		attempt1Resp := session.MakeRequest(t, req, http.StatusOK)
		attempt1View := DecodeJSON(t, attempt1Resp, &actions_web.ViewResponse{})
		assert.EqualValues(t, 1, attempt1View.State.Run.RunAttempt)
		if assert.Len(t, attempt1View.State.Run.Attempts, 2) {
			// attempts ordered by attempt DESC: index 0 = attempt #2 (latest), index 1 = attempt #1 (current)
			assert.False(t, attempt1View.State.Run.Attempts[0].Current)
			assert.True(t, attempt1View.State.Run.Attempts[0].Latest)
			assert.True(t, attempt1View.State.Run.Attempts[1].Current)
			assert.False(t, attempt1View.State.Run.Attempts[1].Latest)
		}
		// isLatestAttempt=false: all write operations disabled
		assert.False(t, attempt1View.State.Run.CanCancel)
		assert.False(t, attempt1View.State.Run.CanApprove)
		assert.False(t, attempt1View.State.Run.CanRerun)
		assert.False(t, attempt1View.State.Run.CanRerunFailed)
		assert.True(t, attempt1View.State.Run.CanDeleteArtifact)
		assert.Equal(t, legacyJob1.ID, attempt1View.State.Run.Jobs[0].ID)
		assert.Equal(t, legacyJob2.ID, attempt1View.State.Run.Jobs[1].ID)
		if assert.Len(t, attempt1View.Artifacts, 1) {
			assert.Equal(t, attempt1View.Artifacts[0].Name, legacyArtifact.ArtifactName)
		}

		// assert the web view for the latest attempt
		req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
		attempt2Resp := session.MakeRequest(t, req, http.StatusOK)
		attempt2View := DecodeJSON(t, attempt2Resp, &actions_web.ViewResponse{})
		assert.EqualValues(t, 2, attempt2View.State.Run.RunAttempt)
		if assert.Len(t, attempt2View.State.Run.Attempts, 2) {
			// attempts ordered by attempt DESC: index 0 = attempt #2 (latest, current), index 1 = attempt #1
			assert.True(t, attempt2View.State.Run.Attempts[0].Current)
			assert.True(t, attempt2View.State.Run.Attempts[0].Latest)
			assert.False(t, attempt2View.State.Run.Attempts[1].Current)
			assert.False(t, attempt2View.State.Run.Attempts[1].Latest)
		}
		// isLatestAttempt=true, done=true: can rerun but not cancel
		assert.False(t, attempt2View.State.Run.CanCancel)
		assert.False(t, attempt2View.State.Run.CanApprove)
		assert.True(t, attempt2View.State.Run.CanRerun)
		assert.False(t, attempt2View.State.Run.CanRerunFailed) // all jobs succeeded
		assert.True(t, attempt2View.State.Run.CanDeleteArtifact)
		assert.Equal(t, rerunJob1.ID, attempt2View.State.Run.Jobs[0].ID)
		assert.Equal(t, rerunJob2.ID, attempt2View.State.Run.Jobs[1].ID)
		assert.Empty(t, attempt2View.Artifacts)
	})
}

type workflowJobPayload struct {
	name    string
	payload []byte
	needs   []string
	runsOn  []string
}

func mustParseSingleWorkflowPayloads(t *testing.T, workflowContent string) map[string]workflowJobPayload {
	t.Helper()

	workflows, err := jobparser.Parse([]byte(workflowContent))
	require.NoError(t, err)

	payloads := make(map[string]workflowJobPayload, len(workflows))
	for _, workflow := range workflows {
		id, job := workflow.Job()
		needs := job.Needs()
		require.NoError(t, workflow.SetJob(id, job.EraseNeeds()))
		payload, err := workflow.Marshal()
		require.NoError(t, err)
		payloads[id] = workflowJobPayload{
			name:    job.Name,
			payload: payload,
			needs:   needs,
			runsOn:  job.RunsOn(),
		}
	}
	return payloads
}

func getRunLatestAttemptNum(t *testing.T, runID int64) int64 {
	t.Helper()

	run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
	attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: run.LatestAttemptID})
	return attempt.Attempt
}
