package runtime

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/automation/blob"
	"github.com/tidbcloud/AgentTeam/internal/automation/dsl"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/store/automation"
)

func TestServerRunnerInjectsEnvSecretsInputsAndMasksLogs(t *testing.T) {
	ctx := t.Context()
	store := newServerRunnerTestStore(t)
	blobStore, err := blob.NewFilesystem(t.TempDir())
	require.NoError(t, err)
	key := []byte("0123456789abcdef0123456789abcdef")
	now := time.Date(2026, 6, 12, 10, 15, 0, 0, time.UTC)
	require.NoError(t, upsertServerRunnerSecretForTest(ctx, store, key, "global_token", "global-secret-value", now))
	require.NoError(t, upsertServerRunnerSecretForTest(ctx, store, key, "step_token", "step-secret-value", now))
	spec := dsl.DefinitionSpec{Name: "Server runner env", Jobs: map[string]dsl.JobSpec{
		"deploy": {
			RunsOn: "ubuntu-latest",
			Env: map[string]any{
				"GLOBAL_TOKEN": "${{ secrets.global_token }}",
				"TARGET":       "${{ inputs.target }}",
				"LITERAL":      "literal",
			},
			Steps: []dsl.StepSpec{
				{
					ID:  "stdout",
					Run: "test -n \"$GLOBAL_TOKEN\"; test -n \"$STEP_TOKEN\"; test \"$TARGET\" = prod; test \"$LITERAL\" = literal; printf '%s:' \"$GLOBAL_TOKEN\"; printf '%s\\n' \"$STEP_TOKEN\"",
					Env: map[string]any{"STEP_TOKEN": "${{ secrets.step_token }}"},
				},
				{
					ID:  "stderr",
					Run: "test -n \"$STEP_TOKEN\"; printf '%s\\n' \"$STEP_TOKEN\" >&2",
					Env: map[string]any{"STEP_TOKEN": "${{ secrets.step_token }}"},
				},
			},
		},
	}}
	run := automation.AutomationRun{ID: "wfr_server_env", WorkspaceID: "ws-server", Name: "Server runner env", Status: automation.AutomationRunStatusRunning, InputJSON: `{"target":"prod"}`, RepositoryOwner: "pingcap", RepositoryName: "agentteam", CreatedAt: now, UpdatedAt: now}
	createServerRunnerRuntime(t, ctx, store, run, spec, now)
	dockerClient := &fakeDockerClient{
		attachOutputs: []fakeDockerAttachOutput{
			{stdout: "global-secret-value:step-secret-value\n"},
			{stderr: "step-secret-value\n"},
		},
	}
	executor, err := newDockerExecutorWithClient(dockerClient, DockerExecutorConfig{Images: map[string]string{"ubuntu-latest": "ubuntu:24.04"}})
	require.NoError(t, err)
	runner := newServerRunnerForTest(t, store, blobStore, ServerRunnerConfig{WorkDir: t.TempDir(), CredentialKey: key, MaxLogChunkBytes: 5, JobTimeout: time.Minute, Executor: executor}, now)

	require.NoError(t, runner.processWorkspace(ctx, "ws-server"))

	jobRuns, err := store.ListJobRuns(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	require.Equal(t, automation.AutomationJobRunStatusSucceeded, jobRuns[0].Status, "error_json=%s", jobRuns[0].ErrorJSON)
	require.NotContains(t, jobRuns[0].InputJSON, "global-secret-value")
	require.NotContains(t, jobRuns[0].InputJSON, "step-secret-value")
	dockerClient.mu.Lock()
	createEnv := append([]string(nil), dockerClient.createConfig.Env...)
	execOptionsLen := len(dockerClient.execOptions)
	var firstStepEnv map[string]string
	var secondStepEnv map[string]string
	if execOptionsLen >= 2 {
		firstStepEnv = envSliceMap(dockerClient.execOptions[0].Env)
		secondStepEnv = envSliceMap(dockerClient.execOptions[1].Env)
	}
	dockerClient.mu.Unlock()
	require.NotContains(t, strings.Join(createEnv, "\n"), "global-secret-value")
	require.Equal(t, 2, execOptionsLen)
	require.Len(t, firstStepEnv, 9)
	require.Len(t, secondStepEnv, 9)
	require.Equal(t, "global-secret-value", firstStepEnv["GLOBAL_TOKEN"])
	require.Equal(t, "step-secret-value", firstStepEnv["STEP_TOKEN"])
	require.Equal(t, "prod", firstStepEnv["TARGET"])
	require.Equal(t, "literal", firstStepEnv["LITERAL"])
	require.Equal(t, "step-secret-value", secondStepEnv["STEP_TOKEN"])
	stdout, err := store.ListLogChunks(ctx, "ws-server", run.ID, jobRuns[0].ID, "stdout", 1, 20)
	require.NoError(t, err)
	require.Greater(t, len(stdout), 1, "small MaxLogChunkBytes should split masked output")
	log := readServerRunnerLogChunks(t, ctx, blobStore, stdout)
	require.Equal(t, "***:***\n", log)
	require.NotContains(t, log, "global-secret-value")
	require.NotContains(t, log, "step-secret-value")
	stderr, err := store.ListLogChunks(ctx, "ws-server", run.ID, jobRuns[0].ID, "stderr", 1, 20)
	require.NoError(t, err)
	stderrLog := readServerRunnerLogChunks(t, ctx, blobStore, stderr)
	require.Equal(t, "***\n", stderrLog)
	require.NotContains(t, stderrLog, "step-secret-value")
}

func TestServerRunnerMasksSecretAcrossStepBoundary(t *testing.T) {
	ctx := t.Context()
	store := newServerRunnerTestStore(t)
	blobStore, err := blob.NewFilesystem(t.TempDir())
	require.NoError(t, err)
	key := []byte("0123456789abcdef0123456789abcdef")
	now := time.Date(2026, 6, 12, 10, 18, 0, 0, time.UTC)
	require.NoError(t, upsertServerRunnerSecretForTest(ctx, store, key, "split_token", "super-secret", now))
	spec := dsl.DefinitionSpec{Name: "Server runner split mask", Jobs: map[string]dsl.JobSpec{
		"deploy": {
			RunsOn: "ubuntu-latest",
			Env:    map[string]any{"TOKEN": "${{ secrets.split_token }}"},
			Steps: []dsl.StepSpec{
				{ID: "prefix", Run: "printf 'super'"},
				{ID: "suffix", Run: "printf '%s\\n' '-secret'"},
			},
		},
	}}
	run := automation.AutomationRun{ID: "wfr_server_split_mask", WorkspaceID: "ws-server", Name: "Server runner split mask", Status: automation.AutomationRunStatusRunning, RepositoryOwner: "pingcap", RepositoryName: "agentteam", CreatedAt: now, UpdatedAt: now}
	createServerRunnerRuntime(t, ctx, store, run, spec, now)
	dockerClient := &fakeDockerClient{
		attachOutputs: []fakeDockerAttachOutput{
			{stdout: "super"},
			{stdout: "-secret\n"},
		},
	}
	executor, err := newDockerExecutorWithClient(dockerClient, DockerExecutorConfig{Images: map[string]string{"ubuntu-latest": "ubuntu:24.04"}})
	require.NoError(t, err)
	runner := newServerRunnerForTest(t, store, blobStore, ServerRunnerConfig{WorkDir: t.TempDir(), CredentialKey: key, JobTimeout: time.Minute, Executor: executor}, now)

	require.NoError(t, runner.processWorkspace(ctx, "ws-server"))

	jobRuns, err := store.ListJobRuns(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	require.Equal(t, automation.AutomationJobRunStatusSucceeded, jobRuns[0].Status, "error_json=%s", jobRuns[0].ErrorJSON)
	stdout, err := store.ListLogChunks(ctx, "ws-server", run.ID, jobRuns[0].ID, "stdout", 1, 20)
	require.NoError(t, err)
	log := readServerRunnerLogChunks(t, ctx, blobStore, stdout)
	require.Equal(t, "***\n", log)
	require.NotContains(t, log, "super-secret")
}

func TestServerRunnerMasksSecretInExecutorFailure(t *testing.T) {
	ctx := t.Context()
	store := newServerRunnerTestStore(t)
	blobStore, err := blob.NewFilesystem(t.TempDir())
	require.NoError(t, err)
	key := []byte("0123456789abcdef0123456789abcdef")
	now := time.Date(2026, 6, 12, 10, 19, 0, 0, time.UTC)
	require.NoError(t, upsertServerRunnerSecretForTest(ctx, store, key, "deploy_token", "executor-secret-value", now))
	spec := dsl.DefinitionSpec{Name: "Server runner failure mask", Jobs: map[string]dsl.JobSpec{
		"deploy": {
			RunsOn: "ubuntu-latest",
			Env:    map[string]any{"TOKEN": "${{ secrets.deploy_token }}"},
			Steps:  []dsl.StepSpec{{Run: "printf should-fail"}},
		},
	}}
	run := automation.AutomationRun{ID: "wfr_server_failure_mask", WorkspaceID: "ws-server", Name: "Server runner failure mask", Status: automation.AutomationRunStatusRunning, RepositoryOwner: "pingcap", RepositoryName: "agentteam", CreatedAt: now, UpdatedAt: now}
	createServerRunnerRuntime(t, ctx, store, run, spec, now)
	executor := fakeJobExecutor{execute: func(context.Context, ExecuteJobRequest) (ExecuteJobResult, error) {
		return ExecuteJobResult{}, fmt.Errorf("executor returned executor-secret-value")
	}}
	runner := newServerRunnerForTest(t, store, blobStore, ServerRunnerConfig{WorkDir: t.TempDir(), CredentialKey: key, JobTimeout: time.Minute, Executor: executor}, now)

	require.NoError(t, runner.processWorkspace(ctx, "ws-server"))

	jobRuns, err := store.ListJobRuns(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	require.Equal(t, automation.AutomationJobRunStatusFailed, jobRuns[0].Status)
	require.NotContains(t, jobRuns[0].ErrorJSON, "executor-secret-value")
	require.Contains(t, jobRuns[0].ErrorJSON, "executor returned ***")
	finished, err := store.GetRun(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.NotContains(t, finished.ErrorJSON, "executor-secret-value")
}

func TestServerRunnerFailsClosedWhenReferencedSecretDisappears(t *testing.T) {
	ctx := t.Context()
	store := newServerRunnerTestStore(t)
	blobStore, err := blob.NewFilesystem(t.TempDir())
	require.NoError(t, err)
	key := []byte("0123456789abcdef0123456789abcdef")
	now := time.Date(2026, 6, 12, 10, 20, 0, 0, time.UTC)
	spec := dsl.DefinitionSpec{Name: "Server runner missing secret", Jobs: map[string]dsl.JobSpec{
		"deploy": {
			RunsOn: "ubuntu-latest",
			Env:    map[string]any{"TOKEN": "${{ secrets.deleted_token }}"},
			Steps:  []dsl.StepSpec{{Run: "printf should-not-run"}},
		},
	}}
	run := automation.AutomationRun{ID: "wfr_server_missing_secret", WorkspaceID: "ws-server", Name: "Server runner missing secret", Status: automation.AutomationRunStatusRunning, RepositoryOwner: "pingcap", RepositoryName: "agentteam", CreatedAt: now, UpdatedAt: now}
	createServerRunnerRuntime(t, ctx, store, run, spec, now)
	runner := newServerRunnerForTest(t, store, blobStore, ServerRunnerConfig{WorkDir: t.TempDir(), CredentialKey: key, JobTimeout: time.Minute}, now)

	require.NoError(t, runner.processWorkspace(ctx, "ws-server"))

	jobRuns, err := store.ListJobRuns(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	require.Equal(t, automation.AutomationJobRunStatusFailed, jobRuns[0].Status)
	require.Contains(t, jobRuns[0].ErrorJSON, "deleted_token")
	require.Contains(t, jobRuns[0].ErrorJSON, "not available")
	stdout, err := store.ListLogChunks(ctx, "ws-server", run.ID, jobRuns[0].ID, "stdout", 1, 20)
	require.NoError(t, err)
	require.Empty(t, stdout)
}

func TestServerRunnerFailsClosedWhenSecretKeyMissing(t *testing.T) {
	ctx := t.Context()
	store := newServerRunnerTestStore(t)
	blobStore, err := blob.NewFilesystem(t.TempDir())
	require.NoError(t, err)
	key := []byte("0123456789abcdef0123456789abcdef")
	now := time.Date(2026, 6, 12, 10, 25, 0, 0, time.UTC)
	require.NoError(t, upsertServerRunnerSecretForTest(ctx, store, key, "deploy_token", "deploy-secret-value", now))
	spec := dsl.DefinitionSpec{Name: "Server runner missing key", Jobs: map[string]dsl.JobSpec{
		"deploy": {
			RunsOn: "ubuntu-latest",
			Env:    map[string]any{"TOKEN": "${{ secrets.deploy_token }}"},
			Steps:  []dsl.StepSpec{{Run: "printf should-not-run"}},
		},
	}}
	run := automation.AutomationRun{ID: "wfr_server_missing_key", WorkspaceID: "ws-server", Name: "Server runner missing key", Status: automation.AutomationRunStatusRunning, RepositoryOwner: "pingcap", RepositoryName: "agentteam", CreatedAt: now, UpdatedAt: now}
	createServerRunnerRuntime(t, ctx, store, run, spec, now)
	runner := newServerRunnerForTest(t, store, blobStore, ServerRunnerConfig{WorkDir: t.TempDir(), JobTimeout: time.Minute}, now)

	require.NoError(t, runner.processWorkspace(ctx, "ws-server"))

	jobRuns, err := store.ListJobRuns(ctx, "ws-server", run.ID)
	require.NoError(t, err)
	require.Len(t, jobRuns, 1)
	require.Equal(t, automation.AutomationJobRunStatusFailed, jobRuns[0].Status)
	require.Contains(t, jobRuns[0].ErrorJSON, "storage is not configured")
	require.NotContains(t, jobRuns[0].ErrorJSON, "deploy-secret-value")
}

func envSliceMap(values []string) map[string]string {
	out := make(map[string]string, len(values))
	for _, value := range values {
		key, val, ok := strings.Cut(value, "=")
		if ok {
			out[key] = val
		}
	}
	return out
}

func upsertServerRunnerSecretForTest(ctx context.Context, store *automation.Store, key []byte, name string, value string, now time.Time) error {
	codec, err := secrets.NewAESGCMCodec(key)
	if err != nil {
		return err
	}
	ciphertext, err := codec.Seal(value)
	if err != nil {
		return err
	}
	_, err = store.UpsertAutomationSecret(ctx, automation.AutomationSecret{
		WorkspaceID:     "ws-server",
		RepositoryOwner: "pingcap",
		RepositoryName:  "agentteam",
		Name:            name,
		ValueCiphertext: ciphertext,
		CreatedBy:       "admin-1",
		UpdatedBy:       "admin-1",
		CreatedAt:       now,
		UpdatedAt:       now,
	})
	return err
}
