package automation

import (
	"context"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

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

func TestCreateRunWithRuntimeEnqueuesPendingRepoStatus(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 14, 9, 0, 0, 0, time.UTC)
	run := automationRepoStatusTestRun(now)

	created, err := store.CreateRunWithRuntimeWithCapacity(ctx, 10, run, []AutomationJobRun{automationRepoStatusTestJobRun(run, AutomationJobRunStatusQueued, now)}, nil, nil)

	require.NoError(t, err)
	require.Equal(t, run.ID, created.ID)
	row, err := loadAutomationRepoStatusOutbox(ctx, store, run.WorkspaceID, run.ID, automationRepoStatusContext(run.DefinitionPath))
	require.NoError(t, err)
	require.Equal(t, AutomationRepoStatusStatePending, row.State)
	require.Equal(t, AutomationRepoStatusOutboxStatusPending, row.Status)
	require.EqualValues(t, 1, row.Sequence)
	require.Zero(t, row.Attempts)
	require.Equal(t, "meshx/automation/.agentteam/build.yml", row.Context)
	require.Equal(t, "Automation Build is running", row.Description)
	require.Equal(t, run.CommitSHA, row.CommitSHA)
}

func TestAutomationRepoStatusOutboxSupersededSequenceKeepsLeaseUntilWorkerReturns(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 14, 9, 30, 0, 0, time.UTC)
	run := automationRepoStatusTestRun(now)
	jobRun := automationRepoStatusTestJobRun(run, AutomationJobRunStatusSucceeded, now)
	completed := now.Add(time.Minute)
	leaseUntil := now.Add(5 * time.Minute)
	jobRun.CompletedAt = &completed

	_, err := store.CreateRunWithRuntimeWithCapacity(ctx, 10, run, []AutomationJobRun{jobRun}, nil, nil)
	require.NoError(t, err)
	leased, err := store.LeaseDueAutomationRepoStatuses(ctx, now, "worker-old", leaseUntil, 1)
	require.NoError(t, err)
	require.Len(t, leased, 1)
	require.EqualValues(t, 1, leased[0].Sequence)
	require.Equal(t, AutomationRepoStatusStatePending, leased[0].State)

	reconciled, err := store.CompleteRunIfTerminal(ctx, run.WorkspaceID, run.ID, completed)
	require.NoError(t, err)
	require.True(t, reconciled)

	row, err := loadAutomationRepoStatusOutbox(ctx, store, run.WorkspaceID, run.ID, leased[0].Context)
	require.NoError(t, err)
	require.EqualValues(t, 2, row.Sequence)
	require.Equal(t, AutomationRepoStatusStateSuccess, row.State)
	require.Equal(t, AutomationRepoStatusOutboxStatusPending, row.Status)
	require.Equal(t, "worker-old", row.LeaseOwner)
	require.NotNil(t, row.LeaseExpiresAt)
	require.Equal(t, leaseUntil, *row.LeaseExpiresAt)
	require.Nil(t, row.DeliveredAt)
	require.Equal(t, "Automation Build succeeded", row.Description)

	next, err := store.LeaseDueAutomationRepoStatuses(ctx, completed, "worker-new", completed.Add(time.Minute), 1)
	require.NoError(t, err)
	require.Empty(t, next)

	delivered, err := store.MarkAutomationRepoStatusDelivered(ctx, leased[0], "worker-old", completed.Add(time.Second))
	require.NoError(t, err)
	require.False(t, delivered)

	row, err = loadAutomationRepoStatusOutbox(ctx, store, run.WorkspaceID, run.ID, leased[0].Context)
	require.NoError(t, err)
	require.Empty(t, row.LeaseOwner)
	require.Nil(t, row.LeaseExpiresAt)
	require.Nil(t, row.DeliveredAt)

	next, err = store.LeaseDueAutomationRepoStatuses(ctx, completed.Add(2*time.Second), "worker-new", completed.Add(time.Minute), 1)
	require.NoError(t, err)
	require.Len(t, next, 1)
	require.EqualValues(t, 2, next[0].Sequence)
	require.Equal(t, AutomationRepoStatusStateSuccess, next[0].State)
}

func TestAutomationRepoStatusOutboxRetrySharesCommitContextRow(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 14, 9, 45, 0, 0, time.UTC)
	first := automationRepoStatusTestRun(now)
	first.ID = "wfr_repo_status_old"
	first.Status = AutomationRunStatusFailed
	first.CompletedAt = &now

	_, err := store.CreateRunWithRuntime(ctx, first, nil, nil, nil)
	require.NoError(t, err)
	leaseUntil := now.Add(5 * time.Minute)
	leased, err := store.LeaseDueAutomationRepoStatuses(ctx, now, "worker-old", leaseUntil, 1)
	require.NoError(t, err)
	require.Len(t, leased, 1)
	require.EqualValues(t, 1, leased[0].Sequence)
	require.Equal(t, first.ID, leased[0].RunID)
	require.Equal(t, AutomationRepoStatusStateFailure, leased[0].State)

	retryCreated := now.Add(time.Minute)
	retry := automationRepoStatusTestRun(retryCreated)
	retry.ID = "wfr_repo_status_retry"
	retry.CreatedAt = retryCreated
	retry.UpdatedAt = retryCreated
	retry.StartedAt = &retryCreated
	_, err = store.CreateRunWithRuntimeWithCapacity(ctx, 10, retry, nil, nil, nil)
	require.NoError(t, err)

	row, err := loadAutomationRepoStatusOutboxForCommitContext(ctx, store, retry)
	require.NoError(t, err)
	require.Equal(t, leased[0].ID, row.ID)
	require.Equal(t, retry.ID, row.RunID)
	require.Equal(t, retryCreated, row.RunCreatedAt)
	require.Equal(t, AutomationRepoStatusStatePending, row.State)
	require.EqualValues(t, 2, row.Sequence)
	require.Equal(t, "worker-old", row.LeaseOwner)
	require.NotNil(t, row.LeaseExpiresAt)

	next, err := store.LeaseDueAutomationRepoStatuses(ctx, retryCreated, "worker-new", retryCreated.Add(time.Minute), 1)
	require.NoError(t, err)
	require.Empty(t, next)

	delivered, err := store.MarkAutomationRepoStatusDelivered(ctx, leased[0], "worker-old", retryCreated.Add(time.Second))
	require.NoError(t, err)
	require.False(t, delivered)

	next, err = store.LeaseDueAutomationRepoStatuses(ctx, retryCreated.Add(2*time.Second), "worker-new", retryCreated.Add(time.Minute), 1)
	require.NoError(t, err)
	require.Len(t, next, 1)
	require.Equal(t, retry.ID, next[0].RunID)
	require.EqualValues(t, 2, next[0].Sequence)
	require.Equal(t, AutomationRepoStatusStatePending, next[0].State)
}

func TestAutomationRepoStatusOutboxOlderTerminalRunDoesNotReplaceNewerRun(t *testing.T) {
	store := newAutomationStoreForTest(t)
	ctx := context.Background()
	now := time.Date(2026, 6, 14, 9, 50, 0, 0, time.UTC)
	first := automationRepoStatusTestRun(now)
	first.ID = "wfr_repo_status_first"
	firstJobRun := automationRepoStatusTestJobRun(first, AutomationJobRunStatusSucceeded, now)
	firstJobRun.ID = "wfjr_repo_status_first_build"
	completed := now.Add(2 * time.Minute)
	firstJobRun.CompletedAt = &completed

	_, err := store.CreateRunWithRuntimeWithCapacity(ctx, 10, first, []AutomationJobRun{firstJobRun}, nil, nil)
	require.NoError(t, err)

	secondCreated := now.Add(time.Minute)
	second := automationRepoStatusTestRun(secondCreated)
	second.ID = "wfr_repo_status_second"
	second.CreatedAt = secondCreated
	second.UpdatedAt = secondCreated
	second.StartedAt = &secondCreated
	_, err = store.CreateRunWithRuntimeWithCapacity(ctx, 10, second, nil, nil, nil)
	require.NoError(t, err)

	row, err := loadAutomationRepoStatusOutboxForCommitContext(ctx, store, second)
	require.NoError(t, err)
	require.Equal(t, second.ID, row.RunID)
	require.Equal(t, AutomationRepoStatusStatePending, row.State)
	require.EqualValues(t, 2, row.Sequence)

	reconciled, err := store.CompleteRunIfTerminal(ctx, first.WorkspaceID, first.ID, completed)
	require.NoError(t, err)
	require.True(t, reconciled)

	row, err = loadAutomationRepoStatusOutboxForCommitContext(ctx, store, second)
	require.NoError(t, err)
	require.Equal(t, second.ID, row.RunID)
	require.Equal(t, secondCreated, row.RunCreatedAt)
	require.Equal(t, AutomationRepoStatusStatePending, row.State)
	require.EqualValues(t, 2, row.Sequence)
}

func TestCreateTerminalRunWithRuntimeEnqueuesTerminalRepoStatus(t *testing.T) {
	tests := []struct {
		name            string
		status          string
		wantState       string
		wantDescription string
	}{
		{
			name:            "succeeded",
			status:          AutomationRunStatusSucceeded,
			wantState:       AutomationRepoStatusStateSuccess,
			wantDescription: "Automation Build succeeded",
		},
		{
			name:            "failed",
			status:          AutomationRunStatusFailed,
			wantState:       AutomationRepoStatusStateFailure,
			wantDescription: "Automation Build failed",
		},
		{
			name:            "canceled",
			status:          AutomationRunStatusCanceled,
			wantState:       AutomationRepoStatusStateError,
			wantDescription: "Automation Build was canceled",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := newAutomationStoreForTest(t)
			ctx := context.Background()
			now := time.Date(2026, 6, 14, 10, 0, 0, 0, time.UTC)
			run := automationRepoStatusTestRun(now)
			run.ID = "wfr_repo_status_" + tt.name
			run.Status = tt.status
			run.CompletedAt = &now

			_, err := store.CreateRunWithRuntime(ctx, run, nil, nil, nil)
			require.NoError(t, err)

			row, err := loadAutomationRepoStatusOutbox(ctx, store, run.WorkspaceID, run.ID, automationRepoStatusContext(run.DefinitionPath))
			require.NoError(t, err)
			require.Equal(t, tt.wantState, row.State)
			require.Equal(t, AutomationRepoStatusOutboxStatusPending, row.Status)
			require.Equal(t, tt.wantDescription, row.Description)
		})
	}
}

func TestAutomationRepoStatusOutboxTruncatesOnUTF8Boundary(t *testing.T) {
	now := time.Date(2026, 6, 14, 10, 30, 0, 0, time.UTC)
	run := automationRepoStatusTestRun(now)
	run.Name = strings.Repeat("部署", 100)

	row, ok := automationRepoStatusOutboxForRun(run, AutomationRepoStatusStateSuccess, now)
	require.True(t, ok)
	require.LessOrEqual(t, len(row.Description), maxAutomationRepoStatusDescription)
	require.True(t, utf8.ValidString(row.Description), "description must remain valid UTF-8: %q", row.Description)

	context := automationRepoStatusContext(".agentteam/" + strings.Repeat("部署", 100) + ".yml")
	require.LessOrEqual(t, len(context), maxAutomationRepoStatusContextLength)
	require.True(t, utf8.ValidString(context), "context must remain valid UTF-8: %q", context)
	require.Contains(t, context, ":")
}

func loadAutomationRepoStatusOutbox(ctx context.Context, store *Store, workspaceID string, runID string, context string) (AutomationRepoStatusOutbox, error) {
	var row AutomationRepoStatusOutbox
	err := store.db.WithContext(ctx).
		Where("workspace_id = ? AND run_id = ? AND context = ?", workspaceID, runID, context).
		First(&row).Error
	return row, err
}

func loadAutomationRepoStatusOutboxForCommitContext(ctx context.Context, store *Store, run AutomationRun) (AutomationRepoStatusOutbox, error) {
	var row AutomationRepoStatusOutbox
	err := store.db.WithContext(ctx).
		Where("workspace_id = ? AND repository_owner = ? AND repository_name = ? AND commit_sha = ? AND context = ?", run.WorkspaceID, run.RepositoryOwner, run.RepositoryName, run.CommitSHA, automationRepoStatusContext(run.DefinitionPath)).
		First(&row).Error
	return row, err
}

func automationRepoStatusTestRun(now time.Time) AutomationRun {
	return AutomationRun{
		ID:                   "wfr_repo_status",
		WorkspaceID:          "ws-1",
		SourceKind:           "agent_git_service_repo",
		RepositoryOwner:      "pingcap",
		RepositoryName:       "agentteam",
		RepositoryFullName:   "pingcap/agentteam",
		DefinitionPath:       ".agentteam/build.yml",
		GitRef:               "refs/heads/main",
		CommitSHA:            "1111111111111111111111111111111111111111",
		DefinitionID:         "ags:pingcap/agentteam:.agentteam/build.yml",
		Name:                 "Build",
		Status:               AutomationRunStatusRunning,
		InputJSON:            "{}",
		OutputJSON:           "{}",
		ErrorJSON:            "{}",
		OwnerPrincipalID:     "user-1",
		CreatedByPrincipalID: "user-1",
		StartedAt:            &now,
		CreatedAt:            now,
		UpdatedAt:            now,
	}
}

func automationRepoStatusTestJobRun(run AutomationRun, status string, now time.Time) AutomationJobRun {
	return AutomationJobRun{
		ID:            "wfjr_repo_status_build",
		WorkspaceID:   run.WorkspaceID,
		RunID:         run.ID,
		JobID:         "build",
		JobType:       "github_actions_job",
		Status:        status,
		MaxAttempts:   1,
		RunsOnJSON:    `["ubuntu-latest"]`,
		InputJSON:     "{}",
		OutputJSON:    "{}",
		ErrorJSON:     "{}",
		DependsOnJSON: "[]",
		ScheduledAt:   now,
		CreatedAt:     now,
		UpdatedAt:     now,
	}
}
