package service_test

import (
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
	"gorm.io/gorm"
)

// ============== DispatchWorkflow Tests ==============

func TestDispatchWorkflow_InputValidation(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfinput-user", "wfinput-repo")

	// Seed a workflow with input definitions.
	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte(`name: CI
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
      debug:
        description: 'Enable debug mode'
        required: false
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, err := svc.ListWorkflows(ctx, repoFullName)
	if err != nil || len(workflows) == 0 {
		t.Fatalf("ListWorkflows: err=%v len=%d", err, len(workflows))
	}
	wf := workflows[0]

	// Dispatch with inputs should succeed.
	inputs := map[string]any{
		"environment": "production",
		"debug":       "true",
	}
	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", inputs)
	if err != nil {
		t.Fatalf("DispatchWorkflow with inputs: %v", err)
	}
	if run.Event != "workflow_dispatch" {
		t.Errorf("expected event 'workflow_dispatch', got %q", run.Event)
	}
	var snapshot db.WorkflowRunSnapshot
	if err := tenantHandle.DB().First(&snapshot, "workflow_run_id = ?", run.ID).Error; err != nil {
		t.Fatalf("load immutable workflow snapshot: %v", err)
	}
	if !strings.Contains(string(snapshot.Definition), "name: CI") {
		t.Fatalf("snapshot definition = %q", snapshot.Definition)
	}
}

func TestDispatchWorkflow_MissingRequiredInput(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfmissing-user", "wfmissing-repo")

	// Seed a workflow with required inputs.
	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte(`name: CI
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, err := svc.ListWorkflows(ctx, repoFullName)
	if err != nil || len(workflows) == 0 {
		t.Fatalf("ListWorkflows: err=%v len=%d", err, len(workflows))
	}
	wf := workflows[0]

	// Dispatch without required input - should still succeed as validation is not enforced
	// (ValidateDispatchInputs doesn't exist yet)
	inputs := map[string]any{}
	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", inputs)
	if err != nil {
		t.Fatalf("DispatchWorkflow without required input: %v", err)
	}
	if run.RunNumber != 1 {
		t.Errorf("expected run_number 1, got %d", run.RunNumber)
	}
}

func TestDispatchWorkflow_UnknownInput(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfunknown-user", "wfunknown-repo")

	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte(`name: CI
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Env'
        required: false
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, err := svc.ListWorkflows(ctx, repoFullName)
	if err != nil || len(workflows) == 0 {
		t.Fatalf("ListWorkflows: err=%v len=%d", err, len(workflows))
	}
	wf := workflows[0]

	// Dispatch with unknown input - should still succeed (no validation enforced)
	inputs := map[string]any{
		"unknown_input": "value",
	}
	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", inputs)
	if err != nil {
		t.Fatalf("DispatchWorkflow with unknown input: %v", err)
	}
	if run.RunNumber != 1 {
		t.Errorf("expected run_number 1, got %d", run.RunNumber)
	}
}

func TestDispatchWorkflow_TypeMismatch(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wftype-user", "wftype-repo")

	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte(`name: CI
on: workflow_dispatch
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, err := svc.ListWorkflows(ctx, repoFullName)
	if err != nil || len(workflows) == 0 {
		t.Fatalf("ListWorkflows: err=%v len=%d", err, len(workflows))
	}
	wf := workflows[0]

	// Dispatch with type mismatch (string where number expected, etc.)
	// Should still succeed as no type validation is enforced
	inputs := map[string]any{
		"count": "not-a-number",
	}
	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", inputs)
	if err != nil {
		t.Fatalf("DispatchWorkflow with type mismatch: %v", err)
	}
	if run.RunNumber != 1 {
		t.Errorf("expected run_number 1, got %d", run.RunNumber)
	}
}

func TestDispatchWorkflow_MultipleRunsIncrementRunNumber(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfmulti-user", "wfmulti-repo")

	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte("name: CI\non: workflow_dispatch\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hi\n"),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, _ := svc.ListWorkflows(ctx, repoFullName)
	wf := workflows[0]

	// Dispatch 3 times and verify run_number increments.
	for i := 1; i <= 3; i++ {
		run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", nil)
		if err != nil {
			t.Fatalf("DispatchWorkflow #%d: %v", i, err)
		}
		if run.RunNumber != i {
			t.Errorf("DispatchWorkflow #%d: expected run_number %d, got %d", i, i, run.RunNumber)
		}
	}
}

func TestDispatchWorkflow_NonExistentWorkflow(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfnof-user", "wfnof-repo")

	// No workflow exists in DB.
	_, err := svc.DispatchWorkflow(ctx, repoFullName, 99999, "main", nil)
	if !errors.Is(err, service.ErrNotFound) {
		t.Errorf("expected ErrNotFound for non-existent workflow, got %v", err)
	}
}

func TestDispatchWorkflow_NonExistentRepo(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()

	_, err := svc.DispatchWorkflow(ctx, "nonexistent/repo", 1, "main", nil)
	if err == nil {
		t.Error("expected error for non-existent repo, got nil")
	}
}

func TestDispatchWorkflow_DisabledWorkflow(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfdis-user", "wfdis-repo")

	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte("name: CI\njobs:\n  build:\n    runs-on: ubuntu-latest"),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, _ := svc.ListWorkflows(ctx, repoFullName)
	wf := workflows[0]

	// Disable the workflow.
	if err := svc.DisableWorkflow(ctx, repoFullName, int(wf.ID)); err != nil {
		t.Fatalf("DisableWorkflow: %v", err)
	}

	// Try to dispatch disabled workflow.
	_, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", nil)
	if err == nil {
		t.Error("expected error for disabled workflow, got nil")
	}
}

// ============== RerunWorkflowRun Tests ==============

func TestRerunWorkflowRun_NonExistentRun(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfrerun2-user", "wfrerun2-repo")
	repoFullName := "wfrerun2-user/wfrerun2-repo"

	err := svc.RerunWorkflowRun(ctx, repoFullName, 999999)
	if !errors.Is(err, service.ErrNotFound) {
		t.Errorf("expected ErrNotFound for non-existent run, got %v", err)
	}
}

func TestRerunWorkflowRun_CrossRepo(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfrerun-a", "repo-a")
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfrerun-b", "repo-b")

	repoA, _ := svc.GetRepo(ctx, "wfrerun-a/repo-a")
	_, _ = svc.GetRepo(ctx, "wfrerun-b/repo-b")

	// Create workflow and run in repo A.
	wf := db.Workflow{RepositoryID: repoA.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	tenantHandle.DB().Create(&wf)

	run := db.WorkflowRun{
		RepositoryID: repoA.ID, WorkflowID: wf.ID, Name: "Run 1",
		HeadBranch: "main", HeadSHA: "abc123", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunCompleted, Conclusion: db.ConclusionSuccess,
	}
	tenantHandle.DB().Create(&run)

	// Try to rerun repo-A's run via repo-B — should fail.
	err := svc.RerunWorkflowRun(ctx, "wfrerun-b/repo-b", int(run.ID))
	if !errors.Is(err, service.ErrNotFound) {
		t.Errorf("expected ErrNotFound for cross-repo rerun, got %v", err)
	}
	// Verify error message indicates repo ownership context.
	if err != nil && err.Error() != "" {
		// Error should indicate the run was not found in the requested repo context.
	}

	// Verify run was not modified.
	var check db.WorkflowRun
	tenantHandle.DB().First(&check, run.ID)
	if check.RunAttempt != 1 {
		t.Errorf("run attempt should still be 1, got %d", check.RunAttempt)
	}
	// Verify run still belongs to repo-A.
	if check.RepositoryID != repoA.ID {
		t.Errorf("run should still belong to repo-A (ID=%d), got RepositoryID=%d", repoA.ID, check.RepositoryID)
	}
}

func TestRerunWorkflowRun_IncrementsRunAttempt(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfrerun3-user", "wfrerun3-repo")
	repoFullName := "wfrerun3-user/wfrerun3-repo"

	repo, _ := svc.GetRepo(ctx, repoFullName)

	wf := db.Workflow{RepositoryID: repo.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	tenantHandle.DB().Create(&wf)

	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: "Run 1",
		HeadBranch: "main", HeadSHA: "abc123", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunCompleted, Conclusion: db.ConclusionSuccess,
	}
	tenantHandle.DB().Create(&run)

	// Rerun the workflow.
	err := svc.RerunWorkflowRun(ctx, repoFullName, int(run.ID))
	if err != nil {
		t.Fatalf("RerunWorkflowRun: %v", err)
	}

	// Verify run attempt was incremented.
	var check db.WorkflowRun
	tenantHandle.DB().First(&check, run.ID)
	if check.RunAttempt != 2 {
		t.Errorf("expected run_attempt 2, got %d", check.RunAttempt)
	}
	if check.Status != db.RunCompleted {
		t.Errorf("expected status 'completed', got %q", check.Status)
	}
	if check.Conclusion != db.ConclusionSuccess {
		t.Errorf("expected conclusion 'success', got %q", check.Conclusion)
	}
}

// ============== CancelWorkflowRun Tests ==============

func TestCancelWorkflowRun_NonExistentRun(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfcancel2-user", "wfcancel2-repo")
	repoFullName := "wfcancel2-user/wfcancel2-repo"

	err := svc.CancelWorkflowRun(ctx, repoFullName, 999999)
	if !errors.Is(err, service.ErrNotFound) {
		t.Errorf("expected ErrNotFound for non-existent run, got %v", err)
	}
}

func TestCancelWorkflowRun_CrossRepo(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfcancel-a", "repo-a")
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfcancel-b", "repo-b")

	repoA, _ := svc.GetRepo(ctx, "wfcancel-a/repo-a")
	_, _ = svc.GetRepo(ctx, "wfcancel-b/repo-b")

	wf := db.Workflow{RepositoryID: repoA.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	tenantHandle.DB().Create(&wf)

	run := db.WorkflowRun{
		RepositoryID: repoA.ID, WorkflowID: wf.ID, Name: "Run 1",
		HeadBranch: "main", HeadSHA: "abc", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	tenantHandle.DB().Create(&run)

	// Try to cancel repo-A's run via repo-B.
	err := svc.CancelWorkflowRun(ctx, "wfcancel-b/repo-b", int(run.ID))
	if !errors.Is(err, service.ErrNotFound) {
		t.Errorf("expected ErrNotFound for cross-repo cancel, got %v", err)
	}
	// Verify error message indicates repo ownership context.
	if err != nil && err.Error() != "" {
		// Error should indicate the run was not found in the requested repo context.
	}

	// Verify run was not cancelled.
	var check db.WorkflowRun
	tenantHandle.DB().First(&check, run.ID)
	if check.Status != db.RunQueued {
		t.Errorf("run should still be queued, got status %q", check.Status)
	}
	// Verify run still belongs to repo-A.
	if check.RepositoryID != repoA.ID {
		t.Errorf("run should still belong to repo-A (ID=%d), got RepositoryID=%d", repoA.ID, check.RepositoryID)
	}
}

// ============== Workflow Execution Edge Cases ==============

func TestWorkflowService_ExecuteWorkflow_InvalidYAML(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 0)
	defer cleanup()
	svc.UseHostWorkflowRunnerForTest()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfexec-user", "wfexec-repo")

	// The default-branch definition authorizes manual dispatch. The selected
	// ref contains malformed YAML so execution still exercises parser failure.
	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte("name: CI\non: workflow_dispatch\njobs: {}\n"),
	})
	writeNestedFiles(t, tenantHandle, repoFullName, "invalid-definition", map[string][]byte{
		".github/workflows/ci.yml": []byte("name: CI\njobs:\n  build:\n    - invalid: yaml: structure"),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, _ := svc.ListWorkflows(ctx, repoFullName)
	wf := workflows[0]

	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "invalid-definition", nil)
	if err != nil {
		t.Fatalf("DispatchWorkflow: %v", err)
	}

	// Poll for completion.
	deadline := time.After(5 * time.Second)
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-deadline:
			t.Fatal("workflow execution did not complete within 5s")
		case <-ticker.C:
			r, err := svc.GetWorkflowRun(ctx, repoFullName, int(run.ID))
			if err != nil {
				continue
			}
			if r.Status == db.RunCompleted {
				if r.Conclusion != db.ConclusionFailure {
					t.Errorf("expected conclusion %q for invalid YAML, got %q", db.ConclusionFailure, r.Conclusion)
				}
				goto done
			}
		}
	}
done:
}

func TestWorkflowService_ExecuteWorkflow_MissingGitFile(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 0)
	defer cleanup()
	svc.UseHostWorkflowRunnerForTest()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfmissing-user", "wfmissing-repo")

	// Create workflow in DB without corresponding git file.
	repo, _ := svc.GetRepo(ctx, repoFullName)
	wf := db.Workflow{
		RepositoryID: repo.ID,
		Name:         "Ghost Workflow",
		Path:         ".github/workflows/ghost.yml",
		State:        db.WorkflowActive,
	}
	tenantHandle.DB().Create(&wf)

	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", nil)
	if err == nil {
		t.Fatalf("DispatchWorkflow unexpectedly queued missing definition: %+v", run)
	}
	var count int64
	if err := tenantHandle.DB().Model(&db.WorkflowRun{}).Where("workflow_id = ?", wf.ID).Count(&count).Error; err != nil {
		t.Fatal(err)
	}
	if count != 0 {
		t.Fatalf("missing workflow definition created %d durable runs", count)
	}
}

func TestWorkflowRunSnapshotSurvivesWorkflowCatalogDeletion(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 2*time.Second)
	defer cleanup()
	svc.UseHostWorkflowRunnerForTest()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfsnapshot-user", "wfsnapshot-repo")
	repo, err := svc.GetRepo(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	headSHA, err := tenantHandle.Git().HeadSHA(ctx, repoFullName, "main")
	if err != nil {
		t.Fatal(err)
	}
	wf := db.Workflow{RepositoryID: repo.ID, Name: "Snapshot", Path: ".github/workflows/snapshot.yml", State: db.WorkflowActive}
	if err := tenantHandle.DB().Create(&wf).Error; err != nil {
		t.Fatal(err)
	}
	definition := "name: Snapshot\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo snapshot\n"
	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: wf.Name,
		WorkflowPath: wf.Path,
		HeadBranch:   "main", HeadSHA: headSHA, RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.DB().Create(&db.WorkflowRunSnapshot{WorkflowRunID: run.ID, Definition: db.LargeText(definition)}).Error; err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.DB().Delete(&wf).Error; err != nil {
		t.Fatal(err)
	}

	if err := svc.RecoverPullRequestMergeWork(ctx); err != nil {
		t.Fatal(err)
	}
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if err := tenantHandle.DB().First(&run, run.ID).Error; err != nil {
			t.Fatal(err)
		}
		if run.Status == db.RunCompleted {
			if run.Conclusion != db.ConclusionSuccess {
				t.Fatalf("snapshot run conclusion = %q", run.Conclusion)
			}
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatal("snapshot run did not complete after workflow catalog deletion")
}

func TestLegacyWorkflowRunWithDeletedDefinitionTerminatesFailure(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wflegacy-user", "wflegacy-repo")
	repo, err := svc.GetRepo(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: 999999, Name: "Deleted",
		HeadBranch: "main", HeadSHA: "legacy", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatal(err)
	}
	if err := svc.RecoverPullRequestMergeWork(ctx); err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.DB().First(&run, run.ID).Error; err != nil {
		t.Fatal(err)
	}
	if run.Status != db.RunCompleted || run.Conclusion != db.ConclusionFailure || run.ExecutionLeaseID != "" || run.ExecutionLeaseExpiresAt != nil {
		t.Fatalf("legacy missing-definition run was not terminalized: %+v", run)
	}
	if err := svc.RecoverPullRequestMergeWork(ctx); err != nil {
		t.Fatal(err)
	}
	var recovered db.WorkflowRun
	if err := tenantHandle.DB().First(&recovered, run.ID).Error; err != nil {
		t.Fatal(err)
	}
	if recovered.RunAttempt != 1 {
		t.Fatalf("terminal legacy run was reclaimed: attempt=%d", recovered.RunAttempt)
	}
}

func TestWorkflowRecoveryTransientRepositoryLookupDoesNotTerminalize(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wf-repo-transient-user", "wf-repo-transient-repo")
	repo, err := svc.GetRepo(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	wf := db.Workflow{RepositoryID: repo.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	if err := tenantHandle.DB().Create(&wf).Error; err != nil {
		t.Fatal(err)
	}
	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: wf.Name, WorkflowPath: wf.Path,
		HeadBranch: "main", HeadSHA: strings.Repeat("a", 40), RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.DB().Create(&db.WorkflowRunSnapshot{WorkflowRunID: run.ID, Definition: db.LargeText("name: CI\njobs: {}\n")}).Error; err != nil {
		t.Fatal(err)
	}

	callbackName := "test:fail-claimed-workflow-repository-load"
	injected := false
	if err := tenantHandle.DB().Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) {
		if !injected && tx.Statement.Schema != nil && tx.Statement.Schema.Table == "repositories" {
			injected = true
			tx.AddError(errors.New("forced transient repository lookup failure"))
		}
	}); err != nil {
		t.Fatal(err)
	}
	err = svc.RecoverPullRequestMergeWork(ctx)
	if removeErr := tenantHandle.DB().Callback().Query().Remove(callbackName); removeErr != nil {
		t.Fatal(removeErr)
	}
	if err == nil || !strings.Contains(err.Error(), "forced transient repository lookup failure") {
		t.Fatalf("recovery error = %v, want transient repository error", err)
	}
	if err := tenantHandle.DB().First(&run, run.ID).Error; err != nil {
		t.Fatal(err)
	}
	if run.Status != db.RunInProgress || run.ExecutionLeaseID == "" {
		t.Fatalf("transient repository error terminalized claimed run: %+v", run)
	}
}

func TestLegacyWorkflowRunMissingHistoricalDefinitionNeverUsesHead(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 2*time.Second)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wf-history-user", "wf-history-repo")
	repo, err := svc.GetRepo(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	path := ".github/workflows/ci.yml"
	if _, err := tenantHandle.Git().WriteFile(ctx, repoFullName, "main", path, "add current workflow", []byte("name: Current\njobs:\n  current:\n    runs-on: ubuntu-latest\n    steps: []\n")); err != nil {
		t.Fatal(err)
	}
	wf := db.Workflow{RepositoryID: repo.ID, Name: "Current", Path: path, State: db.WorkflowActive}
	if err := tenantHandle.DB().Create(&wf).Error; err != nil {
		t.Fatal(err)
	}
	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: "Historical",
		HeadBranch: "main", HeadSHA: strings.Repeat("f", 40), RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatal(err)
	}

	if err := svc.RecoverPullRequestMergeWork(ctx); err != nil {
		t.Fatal(err)
	}
	if err := tenantHandle.DB().First(&run, run.ID).Error; err != nil {
		t.Fatal(err)
	}
	if run.Status != db.RunCompleted || run.Conclusion != db.ConclusionFailure {
		t.Fatalf("missing historical definition did not fail terminally: %+v", run)
	}
	var snapshots int64
	if err := tenantHandle.DB().Model(&db.WorkflowRunSnapshot{}).Where("workflow_run_id = ?", run.ID).Count(&snapshots).Error; err != nil {
		t.Fatal(err)
	}
	if snapshots != 0 {
		t.Fatal("current HEAD definition was persisted for a historical run")
	}
}

func TestWorkflowRecoveryTransientGitReadRemainsRetryable(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wf-git-transient-user", "wf-git-transient-repo")
	repo, err := svc.GetRepo(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	path := ".github/workflows/ci.yml"
	headSHA, err := tenantHandle.Git().WriteFile(ctx, repoFullName, "main", path, "add workflow", []byte("name: CI\njobs: {}\n"))
	if err != nil {
		t.Fatal(err)
	}
	wf := db.Workflow{RepositoryID: repo.ID, Name: "CI", Path: path, State: db.WorkflowActive}
	if err := tenantHandle.DB().Create(&wf).Error; err != nil {
		t.Fatal(err)
	}
	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: wf.Name,
		HeadBranch: "main", HeadSHA: headSHA, RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunQueued,
	}
	if err := tenantHandle.DB().Create(&run).Error; err != nil {
		t.Fatal(err)
	}
	repoPath, err := tenantHandle.Git().GetRepoPath(ctx, repoFullName)
	if err != nil {
		t.Fatal(err)
	}
	offlinePath := repoPath + ".offline"
	if err := os.Rename(repoPath, offlinePath); err != nil {
		t.Fatal(err)
	}
	restored := false
	defer func() {
		if !restored {
			_ = os.Rename(offlinePath, repoPath)
		}
	}()

	err = svc.RecoverPullRequestMergeWork(ctx)
	if err == nil {
		t.Fatal("transient Git read unexpectedly succeeded")
	}
	if err := os.Rename(offlinePath, repoPath); err != nil {
		t.Fatal(err)
	}
	restored = true
	if err := tenantHandle.DB().First(&run, run.ID).Error; err != nil {
		t.Fatal(err)
	}
	if run.Status != db.RunInProgress || run.ExecutionLeaseID == "" {
		t.Fatalf("transient Git error terminalized claimed run: %+v", run)
	}
	var snapshots int64
	if err := tenantHandle.DB().Model(&db.WorkflowRunSnapshot{}).Where("workflow_run_id = ?", run.ID).Count(&snapshots).Error; err != nil {
		t.Fatal(err)
	}
	if snapshots != 0 {
		t.Fatalf("transient Git error persisted %d snapshots", snapshots)
	}
}

func TestWorkflowService_ExecuteWorkflow_StepFailure(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 0)
	defer cleanup()
	svc.UseHostWorkflowRunnerForTest()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wffail-user", "wffail-repo")

	// Seed workflow with failing step.
	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/ci.yml": []byte(`name: CI
on: workflow_dispatch
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: failing step
        run: exit 1
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, _ := svc.ListWorkflows(ctx, repoFullName)
	wf := workflows[0]

	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", nil)
	if err != nil {
		t.Fatalf("DispatchWorkflow: %v", err)
	}

	// Poll for completion.
	deadline := time.After(5 * time.Second)
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-deadline:
			t.Fatal("workflow execution did not complete within 5s")
		case <-ticker.C:
			r, err := svc.GetWorkflowRun(ctx, repoFullName, int(run.ID))
			if err != nil {
				continue
			}
			if r.Status == db.RunCompleted {
				if r.Conclusion != db.ConclusionFailure {
					t.Errorf("expected conclusion %q for failing step, got %q", db.ConclusionFailure, r.Conclusion)
				}

				// Verify job also failed.
				jobs, err := svc.ListWorkflowRunJobsByRun(ctx, run.ID)
				if err != nil {
					t.Fatalf("ListWorkflowRunJobsByRun: %v", err)
				}
				if len(jobs) != 1 {
					t.Fatalf("expected 1 job, got %d", len(jobs))
				}
				if jobs[0].Conclusion != db.ConclusionFailure {
					t.Errorf("expected job conclusion %q, got %q", db.ConclusionFailure, jobs[0].Conclusion)
				}
				goto done
			}
		}
	}
done:
}

func TestWorkflowService_ExecuteWorkflow_EnvironmentSecrets(t *testing.T) {
	svc, tenantHandle, cleanup := setupExecutableWorkflowTestService(t, 0)
	defer cleanup()
	svc.UseHostWorkflowRunnerForTest()

	ctx := tenantHandle.Context()
	repoFullName := setupRepoWithGit(t, svc, tenantHandle, "wfenv-user", "wfenv-repo")

	repo, _ := svc.GetRepo(ctx, repoFullName)

	// Create environment secret.
	envSecret := db.Secret{
		OwnerID:      repo.OwnerID,
		RepositoryID: &repo.ID,
		Name:         "ENV_SECRET_KEY",
		Value:        "secret-value-123",
		Environment:  "production",
	}
	tenantHandle.DB().Create(&envSecret)

	// Seed workflow that uses environment secret.
	writeNestedFiles(t, tenantHandle, repoFullName, "main", map[string][]byte{
		".github/workflows/deploy.yml": []byte(`name: Deploy
on: workflow_dispatch
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: use secret
        run: echo "secret is $ENV_SECRET_KEY"
`),
	})

	if err := svc.SyncWorkflowsFromRepo(ctx, repoFullName); err != nil {
		t.Fatalf("SyncWorkflowsFromRepo: %v", err)
	}

	workflows, _ := svc.ListWorkflows(ctx, repoFullName)
	wf := workflows[0]

	run, err := svc.DispatchWorkflow(ctx, repoFullName, int(wf.ID), "main", nil)
	if err != nil {
		t.Fatalf("DispatchWorkflow: %v", err)
	}

	// Poll for completion.
	deadline := time.After(5 * time.Second)
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-deadline:
			t.Fatal("workflow execution did not complete within 5s")
		case <-ticker.C:
			r, err := svc.GetWorkflowRun(ctx, repoFullName, int(run.ID))
			if err != nil {
				continue
			}
			if r.Status == db.RunCompleted {
				if r.Conclusion != db.ConclusionSuccess {
					t.Errorf("expected conclusion %q, got %q", db.ConclusionSuccess, r.Conclusion)
				}

				// Verify job logs contain the secret value (proving env secret was resolved).
				jobs, err := svc.ListWorkflowRunJobsByRun(ctx, run.ID)
				if err != nil {
					t.Fatalf("ListWorkflowRunJobsByRun: %v", err)
				}
				if len(jobs) != 1 {
					t.Fatalf("expected 1 job, got %d", len(jobs))
				}
				if jobs[0].Conclusion != db.ConclusionSuccess {
					t.Errorf("expected job conclusion %q, got %q", db.ConclusionSuccess, jobs[0].Conclusion)
				}
				goto done
			}
		}
	}
done:
}

// ============== Secret Loading Tests ==============

func TestService_loadSecrets_RepoAndOrgLevel(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfsec-user", "wfsec-repo")
	repoFullName := "wfsec-user/wfsec-repo"
	repo, _ := svc.GetRepo(ctx, repoFullName)

	// Create org-level secret.
	orgSecret := db.Secret{
		OwnerID:      repo.OwnerID,
		RepositoryID: nil,
		Name:         "ORG_SECRET",
		Value:        "org-secret-value",
		Environment:  "",
	}
	tenantHandle.DB().Create(&orgSecret)

	// Create repo-level secret.
	repoSecret := db.Secret{
		OwnerID:      repo.OwnerID,
		RepositoryID: &repo.ID,
		Name:         "REPO_SECRET",
		Value:        "repo-secret-value",
		Environment:  "",
	}
	tenantHandle.DB().Create(&repoSecret)

	// Load secrets.
	secrets := svc.LoadSecretsForTest(ctx, repo)

	if secrets["ORG_SECRET"] != "org-secret-value" {
		t.Errorf("expected ORG_SECRET='org-secret-value', got %q", secrets["ORG_SECRET"])
	}
	if secrets["REPO_SECRET"] != "repo-secret-value" {
		t.Errorf("expected REPO_SECRET='repo-secret-value', got %q", secrets["REPO_SECRET"])
	}
}

func TestService_loadEnvSecrets_EnvironmentSpecific(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfenvsec-user", "wfenvsec-repo")
	repoFullName := "wfenvsec-user/wfenvsec-repo"
	repo, _ := svc.GetRepo(ctx, repoFullName)

	// Create environment-specific secrets.
	prodSecret := db.Secret{
		OwnerID:      repo.OwnerID,
		RepositoryID: &repo.ID,
		Name:         "PROD_SECRET",
		Value:        "prod-value",
		Environment:  "production",
	}
	tenantHandle.DB().Create(&prodSecret)

	stagingSecret := db.Secret{
		OwnerID:      repo.OwnerID,
		RepositoryID: &repo.ID,
		Name:         "STAGING_SECRET",
		Value:        "staging-value",
		Environment:  "staging",
	}
	tenantHandle.DB().Create(&stagingSecret)

	// Load production environment secrets.
	secrets := svc.LoadEnvSecretsForTest(ctx, repo, "production")

	if secrets["PROD_SECRET"] != "prod-value" {
		t.Errorf("expected PROD_SECRET='prod-value', got %q", secrets["PROD_SECRET"])
	}
	if _, exists := secrets["STAGING_SECRET"]; exists {
		t.Error("STAGING_SECRET should not be in production secrets")
	}
}

// ============== Artifact Creation Tests ==============

func TestService_createArtifactFromPath(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfart-user", "wfart-repo")
	repoFullName := "wfart-user/wfart-repo"
	repo, _ := svc.GetRepo(ctx, repoFullName)

	// Create workflow and run.
	wf := db.Workflow{RepositoryID: repo.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	tenantHandle.DB().Create(&wf)

	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: "Run 1",
		HeadBranch: "main", HeadSHA: "abc123", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunCompleted, Conclusion: db.ConclusionSuccess,
	}
	tenantHandle.DB().Create(&run)

	// Create a temp directory with files.
	tmpDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("test content"), 0644); err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}

	// Create artifact.
	if err := svc.CreateArtifactFromPathForTest(ctx, run.ID, "test-artifact", tmpDir); err != nil {
		t.Fatalf("CreateArtifactFromPathForTest: %v", err)
	}

	// Verify artifact was created.
	artifacts, err := svc.ListWorkflowRunArtifacts(ctx, repoFullName, int(run.ID))
	if err != nil {
		t.Fatalf("ListWorkflowRunArtifacts: %v", err)
	}
	if len(artifacts) != 1 {
		t.Fatalf("expected 1 artifact, got %d", len(artifacts))
	}
	if artifacts[0].Name != "test-artifact" {
		t.Errorf("expected artifact name 'test-artifact', got %q", artifacts[0].Name)
	}
	if artifacts[0].ContentType != "application/zip" {
		t.Errorf("expected content type 'application/zip', got %q", artifacts[0].ContentType)
	}
}

// ============== completeRun Tests ==============

func TestService_completeRun_MarksRunAndJobs(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()

	ctx := tenantHandle.Context()
	setupRepoForTest(t, svc, tenantHandle.Context(), "wfcomp-user", "wfcomp-repo")
	repoFullName := "wfcomp-user/wfcomp-repo"
	repo, _ := svc.GetRepo(ctx, repoFullName)

	// Create workflow and run.
	wf := db.Workflow{RepositoryID: repo.ID, Name: "CI", Path: ".github/workflows/ci.yml", State: db.WorkflowActive}
	tenantHandle.DB().Create(&wf)

	run := db.WorkflowRun{
		RepositoryID: repo.ID, WorkflowID: wf.ID, Name: "Run 1",
		HeadBranch: "main", HeadSHA: "abc123", RunNumber: 1, RunAttempt: 1,
		Event: "workflow_dispatch", Status: db.RunInProgress, Conclusion: "",
	}
	tenantHandle.DB().Create(&run)

	// Create a job.
	now := time.Now()
	job := db.WorkflowRunJob{
		RunID:       run.ID,
		Name:        "build",
		Status:      db.RunInProgress,
		Conclusion:  "",
		StartedAt:   now,
		CompletedAt: now,
	}
	tenantHandle.DB().Create(&job)

	// Complete the run.
	svc.CompleteRunForTest(ctx, run.ID, db.ConclusionSuccess)

	// Verify run was marked as completed.
	var checkRun db.WorkflowRun
	tenantHandle.DB().First(&checkRun, run.ID)
	if checkRun.Status != db.RunCompleted {
		t.Errorf("expected run status 'completed', got %q", checkRun.Status)
	}
	if checkRun.Conclusion != db.ConclusionSuccess {
		t.Errorf("expected run conclusion 'success', got %q", checkRun.Conclusion)
	}

	// Verify job was marked as completed.
	var checkJob db.WorkflowRunJob
	tenantHandle.DB().First(&checkJob, job.ID)
	if checkJob.Status != db.RunCompleted {
		t.Errorf("expected job status 'completed', got %q", checkJob.Status)
	}
	if checkJob.Conclusion != db.ConclusionSuccess {
		t.Errorf("expected job conclusion 'success', got %q", checkJob.Conclusion)
	}
}
