package exec

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"
)

func TestWorkflowStepPayloadReadRoundTripsUnderExecResultBudget(t *testing.T) {
	full := `{"payload":"` + strings.Repeat("x", 70000) + `"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body workflowStepTransportRequest
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatal(err)
		}
		if body.Action != "get" || body.WorkflowRunID != "wri_1" || body.WorkflowStepID != "wfx_1" {
			t.Fatalf("unexpected callback read request: %+v", body)
		}
		if body.Section == "" {
			_ = json.NewEncoder(w).Encode(map[string]any{
				"runId": "wri_1", "stepId": "wfx_1", "status": "awaiting", "runStatus": "waiting", "replayed": false,
				"inputRef": map[string]any{"cursor": "c:0"},
			})
			return
		}
		if body.Section != "input" {
			t.Fatalf("unexpected callback payload section: %+v", body)
		}
		offset := 0
		if body.Cursor != "" {
			parsed, err := strconv.Atoi(strings.TrimPrefix(body.Cursor, "c:"))
			if err != nil {
				t.Fatal(err)
			}
			offset = parsed
		}
		limit := body.Limit
		if limit == 0 || limit > 2048 {
			limit = 2048
		}
		end := offset + limit
		if end > len(full) {
			end = len(full)
		}
		payload := map[string]any{
			"section": "input", "encoding": "json", "value": full[offset:end],
		}
		if end < len(full) {
			payload["nextCursor"] = "c:" + strconv.Itoa(end)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"runId": "wri_1", "stepId": "wfx_1", "status": "awaiting", "runStatus": "waiting", "replayed": false, "payload": payload,
		})
	}))
	defer server.Close()
	client := workflowTransportE2EClient(server)
	remote := workflowRemote{client: client}

	compact, _, err := remote.step(context.Background(), workflowStepInput{
		Action: "get", RunID: "wri_1", StepID: "wfx_1",
	}, workflowRunContext{WorkspaceID: "wsp_1", AgentID: "agt_1", RunID: "wri_1", EffectID: "wfx_1"})
	if err != nil {
		t.Fatal(err)
	}
	if compact == nil || compact.InputRef == nil {
		t.Fatal("oversized callback input did not expose a continuation ref")
	}
	compactResult := marshalTypedActionResult(actionResult[workflowStepOutcome]{Data: workflowStepOutcome{step: compact}})
	if len(compactResult) > maxSerializedResultBytes || strings.Contains(string(compactResult), "result_budget") {
		t.Fatalf("compact callback result crossed Exec result budget: %d bytes", len(compactResult))
	}

	cursor := compact.InputRef.Cursor
	var recovered strings.Builder
	for {
		view, _, err := remote.step(context.Background(), workflowStepInput{
			Action: "get", RunID: "wri_1", StepID: "wfx_1", Section: "input", Cursor: cursor, Limit: 2048,
		}, workflowRunContext{WorkspaceID: "wsp_1", AgentID: "agt_1", RunID: "wri_1", EffectID: "wfx_1"})
		if err != nil {
			t.Fatal(err)
		}
		if view == nil || view.Payload == nil {
			t.Fatal("payload page missing")
		}
		result := marshalTypedActionResult(actionResult[workflowStepOutcome]{Data: workflowStepOutcome{step: view}})
		if len(result) > maxSerializedResultBytes || strings.Contains(string(result), "result_budget") {
			t.Fatalf("bounded payload page crossed Exec result budget: %d bytes: %s", len(result), result[:min(len(result), 200)])
		}
		recovered.WriteString(view.Payload.Value)
		if view.Payload.NextCursor == "" {
			break
		}
		cursor = view.Payload.NextCursor
	}
	if recovered.String() != full {
		t.Fatalf("payload recovery changed data: got %d bytes, want %d", recovered.Len(), len(full))
	}
}
