package service

import (
	"context"
	"fmt"
	"os/exec"

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

// Test helper methods - exported only for testing purposes.
// These methods provide access to internal functions for test validation.

// ContextWithUserForTest binds user as a principal owned by the tenant already
// present on ctx. It never invents or defaults a tenant.
func ContextWithUserForTest(ctx context.Context, user db.User) (context.Context, error) {
	principal, err := principalForResolvedUser(ctx, user)
	if err != nil {
		return nil, err
	}
	return ContextWithPrincipal(ctx, principal)
}

// LoadSecretsForTest exposes loadSecrets for testing.
func (s *Service) LoadSecretsForTest(ctx context.Context, repo db.Repository) map[string]string {
	return s.loadSecrets(ctx, repo)
}

// LoadEnvSecretsForTest exposes loadEnvSecrets for testing.
func (s *Service) LoadEnvSecretsForTest(ctx context.Context, repo db.Repository, env string) map[string]string {
	return s.loadEnvSecrets(ctx, repo, env)
}

// CreateArtifactFromPathForTest exposes createArtifactFromPath for testing.
func (s *Service) CreateArtifactFromPathForTest(ctx context.Context, runID uint, name, basePath string) error {
	return s.createArtifactFromPath(ctx, runID, name, basePath)
}

// CompleteRunForTest exposes completeRun for testing.
func (s *Service) CompleteRunForTest(ctx context.Context, runID uint, conclusion string) {
	const leaseID = "test-completion-lease"
	if err := s.DBForCtx(ctx).Model(&db.WorkflowRun{}).Where("id = ?", runID).
		Update("execution_lease_id", leaseID).Error; err != nil {
		return
	}
	s.completeRun(ctx, runID, leaseID, conclusion)
}

// SetMergeGroupBeforeRefPublishHookForTest coordinates publication races in
// service integration tests.
func (s *Service) SetMergeGroupBeforeRefPublishHookForTest(hook func(groupID uint)) {
	s.testMergeGroupBeforeRefPublish = hook
}

// UseHostWorkflowRunnerForTest installs a host-shell runner. Tenant execution
// policy must already be present in the immutable fixture resource bundle.
func (s *Service) UseHostWorkflowRunnerForTest() {
	s.workflowStepRunner = workflowStepRunnerFunc(func(ctx context.Context, req workflowStepRequest) (workflowStepResult, error) {
		cmd := exec.CommandContext(ctx, "bash", "-e", "-c", req.Script)
		cmd.Dir = req.Dir
		cmd.Env = append([]string{
			"HOME=" + workflowTmpMount,
			"PATH=" + workflowExecPath,
			"CI=true",
			"GITHUB_ACTIONS=true",
		}, req.Env...)
		out, err := cmd.CombinedOutput()
		result := workflowStepResult{Output: out}
		if err != nil {
			if exitErr, ok := err.(*exec.ExitError); ok {
				result.ExitCode = exitErr.ExitCode()
			} else {
				result.ExitCode = 1
			}
			if ctx.Err() == context.DeadlineExceeded {
				result.TimedOut = true
				result.ExitCode = 1
			}
			return result, err
		}
		return result, nil
	})
}

// SetWorkflowStepRunnerForTest replaces the process-level workflow runner with
// a custom test hook without mutating tenant policy.
func (s *Service) SetWorkflowStepRunnerForTest(fn func(ctx context.Context, dir, script string, env []string) ([]byte, int, error)) {
	s.workflowStepRunner = workflowStepRunnerFunc(func(ctx context.Context, req workflowStepRequest) (workflowStepResult, error) {
		out, exitCode, err := fn(ctx, req.Dir, req.Script, req.Env)
		result := workflowStepResult{
			Output:   out,
			ExitCode: exitCode,
			TimedOut: ctx.Err() == context.DeadlineExceeded,
		}
		if err != nil {
			return result, err
		}
		if exitCode != 0 {
			return result, fmt.Errorf("workflow test runner exit code %d", exitCode)
		}
		return result, nil
	})
}

// SetWikiGitIngestAfterSnapshotHookForTest installs a test-only hook after
// ingestOneWikiGit snapshots catalog/git state and before it replays any git
// commits.
func (s *Service) SetWikiGitIngestAfterSnapshotHookForTest(fn func(repoFullName string)) {
	s.testWikiGitIngestAfterSnapshot = fn
}

// SetWikiBackgroundGitIngestStartedHookForTest installs a test-only hook fired
// when a repo-scoped background wiki git ingest is claimed and scheduled.
func (s *Service) SetWikiBackgroundGitIngestStartedHookForTest(fn func(repoFullName string)) {
	s.testWikiBackgroundGitIngestStarted = fn
}

// IsPublicRepoForTest exposes isPublicRepo to external-package tests.
func IsPublicRepoForTest(s *Service, ctx context.Context, repoID uint) bool {
	return s.isPublicRepo(ctx, repoID)
}

// SetTestWikiCompactRefUpdateFailureForTest installs a test-only hook that can
// force CompactWikiHistory to fail before the compacted catalog state commits.
func SetTestWikiCompactRefUpdateFailureForTest(s *Service, fn func(repoFullName, commitSHA string) error) {
	s.testWikiCompactRefUpdateFailure = fn
}

// SetTestWikiPostCommitEffectsForTest installs a test-only hook at the start
// of ordered post-commit side-effect processing.
func SetTestWikiPostCommitEffectsForTest(s *Service, fn func(repoFullName string, result wikicatalog.ChangeSetResult)) {
	s.testWikiPostCommitEffects = fn
}

// SetTestWikiPreparedPublishFailureForTest installs a test-only hook that can
// force prepared REST commit publication to fail after the catalog transaction.
func SetTestWikiPreparedPublishFailureForTest(s *Service, fn func(repoFullName, commitSHA string) error) {
	s.testWikiPreparedPublishFailure = fn
}

// SetTestWikiPreparedPersistForTest installs a test-only hook immediately
// before a prepared REST commit's objects are persisted.
func SetTestWikiPreparedPersistForTest(s *Service, fn func(repoFullName, commitSHA string) error) {
	s.testWikiPreparedPersist = fn
}

// SetTestWikiRESTSnapshotForTest installs a test-only hook after a REST writer
// captures its catalog snapshot and before it waits for the repository Git lock.
func SetTestWikiRESTSnapshotForTest(s *Service, fn func(repoFullName string)) {
	s.testWikiRESTSnapshot = fn
}

// SetTestWikiReceivePackIngestFailureForTest installs a test-only hook that can
// force the catalog ingest after receive-pack has updated the Git ref to fail.
func SetTestWikiReceivePackIngestFailureForTest(s *Service, fn func(repoFullName string) error) {
	s.testWikiReceivePackIngestFailure = fn
}

// SetTestWikiGitRepairObligationLoadedForTest installs a hook after a wiki
// repair obligation is loaded and before it is consumed or cleared.
func SetTestWikiGitRepairObligationLoadedForTest(s *Service, fn func(repoFullName string, obligation db.WikiGitRepairObligation)) {
	s.testWikiGitRepairObligationLoaded = fn
}

// SetTestWikiCompactionJobStartedForTest installs a test-only hook fired after
// the async compaction worker marks a job running.
func SetTestWikiCompactionJobStartedForTest(s *Service, fn func(jobID string)) {
	s.testWikiCompactionJobStarted = fn
}

// SetTestWikiCompactionJobContinueForTest installs a test-only hook that can
// block the async compaction worker until tests allow it to proceed.
func SetTestWikiCompactionJobContinueForTest(s *Service, fn func(jobID string)) {
	s.testWikiCompactionJobContinue = fn
}

// ClaimWikiBackgroundGitIngestForTest exposes background git ingest slot claims for tests.
func (s *Service) ClaimWikiBackgroundGitIngestForTest(ctx context.Context, repoFullName string) bool {
	repo, err := s.LookupRepoIdentity(ctx, repoFullName)
	if err != nil {
		return false
	}
	key, err := s.wikiRepoKey(ctx, repo)
	if err != nil {
		return false
	}
	return s.claimWikiBackgroundGitIngest(key)
}

// ReleaseWikiBackgroundGitIngestForTest exposes background git ingest cleanup for tests.
func (s *Service) ReleaseWikiBackgroundGitIngestForTest(ctx context.Context, repoFullName string) {
	repo, err := s.LookupRepoIdentity(ctx, repoFullName)
	if err != nil {
		return
	}
	key, err := s.wikiRepoKey(ctx, repo)
	if err != nil {
		return
	}
	s.releaseWikiBackgroundGitIngest(key)
}
