package app

import (
	"context"
	"os"
	"strings"
	"testing"

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

func TestCommandFailureIncludesTailOfStdoutAndStderr(t *testing.T) {
	t.Parallel()
	_, err := commandOutputWithEnv(context.Background(), t.TempDir(), os.Environ(), "sh", "-c", "echo beginning; printf '%0132000d' 0; echo stdout-tail; echo stderr-tail >&2; exit 1")
	require.Error(t, err)
	require.Contains(t, err.Error(), "stdout-tail")
	require.Contains(t, err.Error(), "stderr-tail")

	var output limitedTailBuffer
	_, _ = output.Write([]byte("beginning"))
	_, _ = output.Write([]byte(strings.Repeat("x", maxCommandOutput) + "tail"))
	require.NotContains(t, output.String(), "beginning")
	require.True(t, strings.HasSuffix(output.String(), "tail"))
}

func TestTailTextBoundsValidationFeedback(t *testing.T) {
	t.Parallel()
	value := strings.Repeat("a", 100) + "exact-failure"
	require.Equal(t, "…aaaexact-failure", tailText(value, 16))
}

func TestFreshMainFetchUsesExplicitRemoteTrackingRef(t *testing.T) {
	t.Parallel()
	require.Equal(t, []string{
		"fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main",
	}, freshMainFetchArgs())
}

func TestValidationRepairMayOnlyAddTestsAndMustRetainAdmittedDiff(t *testing.T) {
	t.Parallel()
	before := []string{
		"auto-oncall-services/product-knowledge/index.ndjson",
		"auto-oncall-services/product-knowledge/topics/private-link.md",
	}
	require.NoError(t, validateValidationRepairPaths(before, append(append([]string{}, before...), "auto-oncall-services/test/live-workspace.test.ts")))
	require.ErrorContains(t, validateValidationRepairPaths(before, append(append([]string{}, before...), "auto-oncall-services/src/runner.ts")), "non-test path")
	require.ErrorContains(t, validateValidationRepairPaths(before, before[:1]), "removed an admitted path")
}

func TestAgentEnvironmentNeverPassesOrchestratorSecrets(t *testing.T) {
	workspace := t.TempDir()
	t.Setenv("PATH", "/bin")
	t.Setenv("GH_TOKEN", "github-secret")
	t.Setenv("AUTO_ONCALL_OPTIMIZER_LARK_APP_SECRET", "lark-secret")
	t.Setenv("READ_ONLY_PROVIDER_TOKEN", "read-only")
	runner := NewCodexRunner(Config{CodexHome: "/codex", WorkspaceRoot: workspace, AgentEnvAllowlist: []string{
		"GH_TOKEN", "AUTO_ONCALL_OPTIMIZER_LARK_APP_SECRET", "READ_ONLY_PROVIDER_TOKEN",
	}})

	env := runner.agentEnvironment()
	require.Contains(t, env, "CODEX_HOME=/codex")
	require.Contains(t, env, "HOME="+workspace+"/agent-home")
	require.Contains(t, env, "READ_ONLY_PROVIDER_TOKEN=read-only")
	require.NotContains(t, env, "GH_TOKEN=github-secret")
	require.NotContains(t, env, "AUTO_ONCALL_OPTIMIZER_LARK_APP_SECRET=lark-secret")
	_, ok := os.LookupEnv("GH_TOKEN")
	require.True(t, ok)
}

func TestAgentEnvironmentPassesAllowlistedReadOnlyDatabaseURL(t *testing.T) {
	workspace := t.TempDir()
	t.Setenv("AUTO_ONCALL_DATABASE_URL", "mysql://readonly:secret@database.example/auto_oncall_staging")
	runner := NewCodexRunner(Config{WorkspaceRoot: workspace, AgentEnvAllowlist: []string{
		"AUTO_ONCALL_DATABASE_URL",
	}})

	require.Contains(t, runner.agentEnvironment(), "AUTO_ONCALL_DATABASE_URL=mysql://readonly:secret@database.example/auto_oncall_staging")
}

func TestValidationEnvironmentNeverPassesAgentOrOrchestratorCredentials(t *testing.T) {
	workspace := t.TempDir()
	t.Setenv("PATH", "/bin")
	t.Setenv("GH_TOKEN", "github-secret")
	t.Setenv("CODEX_HOME", "/codex-secret")
	t.Setenv("AUTO_ONCALL_OPTIMIZER_LARK_APP_SECRET", "lark-secret")
	runner := NewCodexRunner(Config{WorkspaceRoot: workspace})

	env := runner.validationEnvironment()
	require.Contains(t, env, "PATH=/bin")
	require.Contains(t, env, "HOME="+workspace+"/validation-home")
	require.NotContains(t, env, "GH_TOKEN=github-secret")
	require.NotContains(t, env, "CODEX_HOME=/codex-secret")
	require.NotContains(t, env, "AUTO_ONCALL_OPTIMIZER_LARK_APP_SECRET=lark-secret")
}

func TestPRTitleIsBounded(t *testing.T) {
	t.Parallel()
	title := prTitle("fix continuation\nextra detail")
	require.Equal(t, "fix(auto-oncall): fix continuation", title)
}

func TestAgentPromptRequiresRuntimeEvidenceAndGapDisclosure(t *testing.T) {
	t.Parallel()
	prompt := agentPrompt(Job{SenderOpenID: "ou_user", Request: "check TCID-13023"})
	require.Contains(t, prompt, "$CODEX_HOME/skills/runtime-evidence/SKILL.md")
	require.Contains(t, prompt, "Never substitute repository templates for deployed state")
	require.Contains(t, prompt, "信息来源与缺口")
	require.Contains(t, prompt, "基于有限证据推断")
}

func TestCodexExecArgsResumesAnalyzeThreadInFreshWorktree(t *testing.T) {
	t.Parallel()
	args := codexExecArgs(
		Job{Mode: JobModeAnalyze, ResumeThreadID: "thread-previous"},
		"/workspace/current-job", "/workspace/result.txt", true, "", "gpt-5.6",
	)
	require.Equal(t, []string{
		"exec", "--model", "gpt-5.6", "--json", "--ignore-user-config",
		"-c", `approval_policy="never"`,
		"-c", `sandbox_workspace_write.network_access=true`,
		"--sandbox", "workspace-write",
		"-C", "/workspace/current-job", "-o", "/workspace/result.txt",
		"resume", "thread-previous", "-",
	}, args)
}

func TestCodexExecArgsNeverResumesWriteMode(t *testing.T) {
	t.Parallel()
	args := codexExecArgs(
		Job{Mode: JobModePR, ResumeThreadID: "thread-analyze"},
		"/workspace/current-job", "/workspace/result.txt", true, "", "",
	)
	require.NotContains(t, args, "resume")
	require.NotContains(t, args, "thread-analyze")
}

func TestCodexExecArgsCanResumeBoundedSingleTicketCorrection(t *testing.T) {
	t.Parallel()
	args := codexExecArgsWithResume(
		"/workspace/current-job", "/workspace/correction.txt", false,
		"/workspace/investigation-schema.json", "gpt-5.6", "thread-investigator",
	)
	require.Equal(t, []string{
		"exec", "--model", "gpt-5.6", "--json", "--ignore-user-config",
		"-c", `approval_policy="never"`,
		"-c", `sandbox_workspace_write.network_access=true`,
		"--sandbox", "read-only",
		"--output-schema", "/workspace/investigation-schema.json",
		"-C", "/workspace/current-job", "-o", "/workspace/correction.txt",
		"resume", "thread-investigator", "-",
	}, args)
}

func TestValidateChangedPaths(t *testing.T) {
	t.Parallel()
	require.NoError(t, validateChangedPaths([]string{
		"auto-oncall-services/src/runner.ts",
		"auto-oncall-services/docs/opts/2026-08-10-fix.md",
	}))
	require.ErrorContains(t, validateChangedPaths([]string{"services/ticket-manager/main.go"}), "outside auto-oncall-services")
	require.ErrorContains(t, validateChangedPaths([]string{"auto-oncall-services/../go.work"}), "outside auto-oncall-services")
}

func TestValidateInvestigationPathsAllowsOnlyFrozenContext(t *testing.T) {
	t.Parallel()
	require.NoError(t, validateInvestigationPaths([]string{singleTicketContextFile}))
	require.ErrorContains(t, validateInvestigationPaths([]string{
		singleTicketContextFile,
		"auto-oncall-services/src/runner.ts",
	}), "investigator changed task worktree")
}
