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

package actions

import (
	"testing"

	actions_model "gitea.dev/models/actions"
	repo_model "gitea.dev/models/repo"
	api "gitea.dev/modules/structs"
	"gitea.dev/modules/timeutil"
	"gitea.dev/modules/translation"

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

func TestViewPullRequestFromRun(t *testing.T) {
	repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}

	t.Run("pull ref", func(t *testing.T) {
		run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/123/head"}
		assert.Equal(t, &ViewPullRequest{Index: "#123", Link: "/owner/repo/pulls/123"}, viewPullRequestFromRun(t.Context(), run, nil))
	})

	t.Run("pull request event payload", func(t *testing.T) {
		// a non-pull ref forces the payload branch instead of the ref branch
		run := &actions_model.ActionRun{Repo: repo, Ref: "refs/heads/feature"}
		payload := &api.PullRequestPayload{Index: 42}
		assert.Equal(t, &ViewPullRequest{Index: "#42", Link: "/owner/repo/pulls/42"}, viewPullRequestFromRun(t.Context(), run, payload))
	})

	t.Run("nil repo", func(t *testing.T) {
		run := &actions_model.ActionRun{Ref: "refs/pull/1/head"}
		assert.Nil(t, viewPullRequestFromRun(t.Context(), run, nil))
	})
}

func TestViewSummaryBranchFromRun(t *testing.T) {
	repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}

	t.Run("pull request event same repo", func(t *testing.T) {
		run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
		payload := &api.PullRequestPayload{
			PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
				Name:       "feature",
				Ref:        "refs/heads/feature",
				RepoID:     1,
				Repository: &api.Repository{Link: "/owner/repo"},
			}},
		}
		assert.Equal(t, ViewBranch{Name: "feature", Link: "/owner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
	})

	t.Run("pull request event from fork prefixes owner", func(t *testing.T) {
		run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
		payload := &api.PullRequestPayload{
			PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
				Name:   "feature",
				Ref:    "refs/heads/feature",
				RepoID: 2,
				Repository: &api.Repository{
					Link:  "/forkowner/repo",
					Owner: &api.User{UserName: "forkowner"},
				},
			}},
		}
		assert.Equal(t, ViewBranch{Name: "forkowner:feature", Link: "/forkowner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
	})

	t.Run("push to tag does not query branch", func(t *testing.T) {
		// a tag ref is not a branch, so no GetBranch DB lookup happens
		run := &actions_model.ActionRun{Repo: repo, Ref: "refs/tags/v1.0.0"}
		assert.Equal(t, ViewBranch{Name: "v1.0.0", Link: "/owner/repo/src/tag/v1.0.0"}, viewSummaryBranchFromRun(t.Context(), run, nil))
	})
}

func TestConvertToViewModel(t *testing.T) {
	task := &actions_model.ActionTask{
		Status: actions_model.StatusSuccess,
		Steps: []*actions_model.ActionTaskStep{
			{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
		},
		Stopped: timeutil.TimeStamp(20),
	}

	viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
	require.NoError(t, err)

	expectedViewJobs := []*ViewJobStep{
		{
			Summary:  "Set up job",
			Duration: "0s",
			Status:   "success",
		},
		{
			Summary:  "Run step-name",
			Duration: "4s",
			Status:   "success",
		},
		{
			Summary:  "Complete job",
			Duration: "15s",
			Status:   "success",
		},
	}
	assert.Equal(t, expectedViewJobs, viewJobSteps)
}

func TestConvertToViewModelCancellingTaskDoesNotRenderRunningSteps(t *testing.T) {
	task := &actions_model.ActionTask{
		Status: actions_model.StatusCancelling,
		Steps: []*actions_model.ActionTaskStep{
			{Name: "Run step-name", Index: 0, Status: actions_model.StatusRunning, LogLength: 1},
		},
	}

	viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
	require.NoError(t, err)

	expectedViewJobs := []*ViewJobStep{
		{
			Summary:  "Set up job",
			Duration: "0s",
			Status:   "success",
		},
		{
			Summary:  "Run step-name",
			Duration: "0s",
			Status:   "cancelling",
		},
		{
			Summary:  "Complete job",
			Duration: "0s",
			Status:   "waiting",
		},
	}
	assert.Equal(t, expectedViewJobs, viewJobSteps)
}

func TestPendingNeeds(t *testing.T) {
	current := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build", "test"}}
	jobs := []*actions_model.ActionRunJob{
		current,
		{JobID: "build", Status: actions_model.StatusSuccess},
		{JobID: "test", Status: actions_model.StatusRunning},
	}
	// "test" is not done yet, "build" succeeded, so only "test" blocks.
	assert.Equal(t, []string{"test"}, pendingNeeds(current, jobs))

	t.Run("all needs done", func(t *testing.T) {
		done := []*actions_model.ActionRunJob{
			current,
			{JobID: "build", Status: actions_model.StatusSuccess},
			{JobID: "test", Status: actions_model.StatusSkipped},
		}
		assert.Empty(t, pendingNeeds(current, done))
	})

	t.Run("matrix expansion all required", func(t *testing.T) {
		matrix := []*actions_model.ActionRunJob{
			current,
			{JobID: "build", Status: actions_model.StatusSuccess},
			{JobID: "build", Status: actions_model.StatusRunning},
			{JobID: "test", Status: actions_model.StatusSuccess},
		}
		assert.Equal(t, []string{"build"}, pendingNeeds(current, matrix))
	})

	t.Run("unresolved need treated as pending", func(t *testing.T) {
		missing := []*actions_model.ActionRunJob{current}
		assert.Equal(t, []string{"build", "test"}, pendingNeeds(current, missing))
	})

	t.Run("parent job scope", func(t *testing.T) {
		// a same-named job under a different parent must not satisfy the need
		scoped := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build"}, ParentJobID: 5}
		jobs := []*actions_model.ActionRunJob{
			scoped,
			{JobID: "build", Status: actions_model.StatusSuccess, ParentJobID: 0},
		}
		assert.Equal(t, []string{"build"}, pendingNeeds(scoped, jobs))
	})
}
