package exec

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

// httpErrorServer spins up a real httptest server that returns the given raw
// JSON error body so the apiclient parses it end-to-end (real HTTP boundary).
func httpErrorServer(t *testing.T, status int, rawBody string) *httptest.Server {
	t.Helper()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !strings.HasPrefix(r.URL.Path, "/api/v1/workspaces/wsp_1/agents/agt_1/runtime/") {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_, _ = w.Write([]byte(rawBody))
	}))
	t.Cleanup(server.Close)
	return server
}

func httpErrorClient(server *httptest.Server) *apiclient.Client {
	return &apiclient.Client{
		ServerURL:        server.URL,
		LocalAgentID:     "local-agent-1",
		AgentID:          "agt_1",
		RouteWorkspaceID: "wsp_1",
		AuthToken:        "agent-token",
		HTTPClient:       server.Client(),
	}
}

func runHTTPWorkflowStep(t *testing.T, remote workflowRemote) error {
	t.Helper()
	_, err := remote.run(context.Background(), workflowRunRequest{Action: "start"})
	return err
}

// rawWorkflowErrorBody renders a server error envelope with the given fields,
// preserving the exact bytes passed (no trimming here — this mimics the raw
// server response).
func rawWorkflowErrorBody(code, stage, path string, diagnostics []map[string]any) string {
	payload, _ := json.Marshal(map[string]any{
		"error":       "bounded server message",
		"code":        code,
		"stage":       stage,
		"path":        path,
		"hint":        "Fix the input.",
		"retryable":   false,
		"details":     map[string]any{"internal_sql": "SELECT secret FROM creds"},
		"diagnostics": diagnostics,
	})
	return string(payload)
}

// TestHTTPBoundaryMalformedTopLevelIdentityFailsClosed proves the real
// HTTP→apiclient→Exec boundary preserves the RAW server code/stage/path bytes
// and classifyAPIError fails closed on a malformed/spaced top-level identity
// instead of trimming it into a valid one.
func TestHTTPBoundaryMalformedTopLevelIdentityFailsClosed(t *testing.T) {
	cases := []struct {
		name  string
		code  string
		stage string
		path  string
	}{
		{"spaced code", " plan_request_failed ", "plan", ""},
		{"malformed code punctuation", "plan_request@failed", "plan", ""},
		{"spaced stage", "plan_request_failed", " plan ", ""},
		{"unknown stage", "plan_request_failed", "not_a_stage", ""},
		{"spaced path", "plan_request_failed", "plan", " $.input.x "},
		{"malformed path", "plan_request_failed", "plan", "password=hunter2 path"},
		{"malformed UTF-8 code", "plan_request_failed\xff", "plan", ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			raw := rawWorkflowErrorBody(tc.code, tc.stage, tc.path, nil)
			server := httpErrorServer(t, http.StatusBadRequest, raw)
			remote := workflowRemote{client: httpErrorClient(server)}
			err := runHTTPWorkflowStep(t, remote)
			classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
			// A malformed identity must never unlock the server contract.
			if classified.ServerCode != "" {
				t.Fatalf("ServerCode = %q, want empty for malformed identity", classified.ServerCode)
			}
			if classified.Stage != "" || classified.Path != "" {
				t.Fatalf("Stage/Path must fail closed: %q / %q", classified.Stage, classified.Path)
			}
			if len(classified.Diagnostics) != 0 {
				t.Fatalf("malformed identity must not unlock diagnostics: %+v", classified.Diagnostics)
			}
		})
	}
}

// TestHTTPBoundaryWrongTypesFailClosed proves a top-level field of the wrong
// JSON type (non-string code/stage/path) fails closed through the real HTTP
// boundary.
func TestHTTPBoundaryWrongTypesFailClosed(t *testing.T) {
	raw := `{"error":"msg","code":42,"stage":{"plan":true},"path":["$.input"],"diagnostics":[]}`
	server := httpErrorServer(t, http.StatusBadRequest, raw)
	remote := workflowRemote{client: httpErrorClient(server)}
	err := runHTTPWorkflowStep(t, remote)
	classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
	if classified.ServerCode != "" {
		t.Fatalf("non-string code must fail closed, ServerCode=%q", classified.ServerCode)
	}
}

// TestHTTPBoundaryValidTopCodeMalformedDiagnostics proves a valid top-level code
// with a malformed/spaced diagnostic code fails closed for that diagnostic
// element while the valid ones survive (multi-element ordering preserved).
func TestHTTPBoundaryValidTopCodeMalformedDiagnostics(t *testing.T) {
	diagnostics := []map[string]any{
		{"severity": "error", "code": "input.unknown_field", "path": "$.input.reviewers[0]", "field": "reviewers", "message": "first"},
		{"severity": "error", "code": " spaced.code ", "path": "$.input", "field": "x", "message": "second"},
		{"severity": "error", "code": "input.other_field", "path": "$.input.items[0]", "field": "items", "message": "third"},
	}
	raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diagnostics)
	server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
	remote := workflowRemote{client: httpErrorClient(server)}
	err := runHTTPWorkflowStep(t, remote)
	classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
	// The spaced-code element is dropped; the two valid elements survive in order.
	if len(classified.Diagnostics) != 2 {
		t.Fatalf("want 2 surviving diagnostics (malformed dropped), got %d", len(classified.Diagnostics))
	}
	if classified.Diagnostics[0].Code != "input.unknown_field" || classified.Diagnostics[1].Code != "input.other_field" {
		t.Fatalf("diagnostic order/identity not preserved: %+v", classified.Diagnostics)
	}
}

// TestHTTPBoundaryValidInputDiagnosticPath proves the invocation input
// diagnostic is unlocked only for an `$.input` exact or segment-correct
// descendant path, and rejects `$.inputSecret` / malformed / secret paths.
func TestHTTPBoundaryValidInputDiagnosticPath(t *testing.T) {
	validPaths := []string{
		"$.input",
		"$.input.reviewers[0]",
		"$.input.items[2].body",
	}
	for _, path := range validPaths {
		t.Run("valid/"+path, func(t *testing.T) {
			diag := []map[string]any{
				{"severity": "error", "code": "input.unknown_field", "path": path, "field": "x", "message": "unknown input field"},
			}
			raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diag)
			server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
			remote := workflowRemote{client: httpErrorClient(server)}
			err := runHTTPWorkflowStep(t, remote)
			classifiedErr := classifyWorkflowOperationError(err, "workflow step complete failed")
			var classified *Error
			if !errors.As(classifiedErr, &classified) {
				t.Fatalf("classified is %T, want *Error", classifiedErr)
			}
			if !strings.Contains(classified.Problem, path) {
				t.Fatalf("valid input path %q must unlock the invocation diagnostic, Problem=%q", path, classified.Problem)
			}
		})
	}

	invalidPaths := []string{
		"$.inputSecret",
		"$.input.malformed path",
		"password=hunter2",
		"$.output.reviewers[0]",
		"$.inputs",
		"$.input.",
	}
	for _, path := range invalidPaths {
		t.Run("invalid/"+path, func(t *testing.T) {
			diag := []map[string]any{
				{"severity": "error", "code": "input.unknown_field", "path": path, "field": "x", "message": "unknown input field"},
			}
			raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diag)
			server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
			remote := workflowRemote{client: httpErrorClient(server)}
			err := runHTTPWorkflowStep(t, remote)
			classifiedErr := classifyWorkflowOperationError(err, "workflow step complete failed")
			var classified *Error
			if !errors.As(classifiedErr, &classified) {
				t.Fatalf("classified is %T, want *Error", classifiedErr)
			}
			if strings.Contains(classified.Problem, path) {
				t.Fatalf("invalid input path %q must NOT unlock the invocation diagnostic, Problem=%q", path, classified.Problem)
			}
		})
	}
}

// TestHTTPBoundaryLegacyDetailsThroughSameGate proves the legacy
// details["diagnostics"] shape routes through the same byte-exact gate as the
// structured diagnostics: a malformed legacy diagnostic fails closed and a
// valid one unlocks only for a segment-correct $.input path.
func TestHTTPBoundaryLegacyDetailsThroughSameGate(t *testing.T) {
	raw := `{
		"error":"invalid input",
		"code":"invocation_input_invalid",
		"stage":"invocation",
		"path":"callback.workItems[2].key",
		"details":{"diagnostics":[
			{"severity":"error","code":"input.unknown_field","path":"$.input.reviewers[0]","message":"unknown field"},
			{"severity":"error","code":" spaced.code ","path":"$.input","message":"malformed"}
		]}
	}`
	server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
	remote := workflowRemote{client: httpErrorClient(server)}
	err := runHTTPWorkflowStep(t, remote)
	diagnostic, ok := workflowInvocationInputDiagnostic(err)
	if !ok {
		t.Fatal("valid legacy invocation input diagnostic must unlock")
	}
	if diagnostic.Path != "$.input.reviewers[0]" {
		t.Fatalf("Path = %q, want $.input.reviewers[0] (malformed element dropped by the same gate)", diagnostic.Path)
	}
}

// TestHTTPBoundaryUnreviewedDynamicCodeFailsClosed proves a syntactically
// resolved but unreviewed invalid_<field> code (one that is NOT in the explicit
// reviewed set) cannot unlock the public contract. Only the concrete reviewed
// invalid_beforeSeq is projected; a novel invalid_<field> value fails closed.
func TestHTTPBoundaryUnreviewedDynamicCodeFailsClosed(t *testing.T) {
	for _, code := range []string{
		"invalid_beforeSeq",  // reviewed concrete code — projects.
		"invalid_otherField", // unreviewed dynamic value — fails closed.
		"invalid_userInput",  // unreviewed — fails closed.
		"invalid_secret=pw",  // malformed — fails closed.
	} {
		t.Run(code, func(t *testing.T) {
			raw := rawWorkflowErrorBody(code, "plan", "", nil)
			server := httpErrorServer(t, http.StatusBadRequest, raw)
			remote := workflowRemote{client: httpErrorClient(server)}
			err := runHTTPWorkflowStep(t, remote)
			classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
			if code == "invalid_beforeSeq" {
				if classified.ServerCode != "invalid_beforeSeq" {
					t.Fatalf("reviewed invalid_beforeSeq must project, ServerCode=%q", classified.ServerCode)
				}
			} else {
				if classified.ServerCode != "" {
					t.Fatalf("unreviewed dynamic code %q must fail closed, ServerCode=%q", code, classified.ServerCode)
				}
				if classified.Problem != "workflow step complete failed" {
					t.Fatalf("unreviewed code must keep the generic redacted problem, got %q", classified.Problem)
				}
			}
		})
	}
}

// TestHTTPBoundaryMalformedInputPathNormalizationTricks proves the segment
// structural grammar rejects empty/double-dot segments, malformed brackets,
// prefix lookalikes, invalid UTF-8, and other normalization tricks.
func TestHTTPBoundaryMalformedInputPathNormalizationTricks(t *testing.T) {
	paths := []string{
		"$.input..reviewers",      // double dot
		"$.input.",                // trailing dot
		"$.input.reviewers[",      // unterminated bracket
		"$.input.reviewers]",      // stray close bracket
		"$.input.reviewers[0",     // unterminated index
		"$.input.reviewers[0]x",   // char after index
		"$.input.reviewers[0][1]", // nested bracket (index then bracket)
		"$.input.reviewers[-1]",   // negative index
		"$.input.reviewers[+1]",   // plus index
		"$.input.reviewers[ 0]",   // spaced index
		"$.input.reviewers[0x1]",  // hex index
		"$.inputSecret",           // prefix lookalike (no dot)
		"$.input_secret",          // underscore after input — not a dot descendant
		"$.input\xff",             // invalid UTF-8
		"$.input.rev\xffiewers",   // invalid UTF-8 in segment
		"$.input.reviewers,path",  // punctuation
		"$.input.reviewers/path",  // slash
		"$.input.reviewers$path",  // dollar
		// Leading-zero indexes.
		"$.input.reviewers[00]",    // leading zero index
		"$.input.reviewers[01]",    // leading zero index
		"$.input.reviewers[007]",   // leading zeros index
		"$.input.reviewers[0][00]", // leading zero in nested index
		// A dot must be followed by a field, never a bracket or another dot.
		"$.input.reviewers[0].",       // trailing dot after index
		"$.input.reviewers[0].[1]",    // bracket after a dot (no field)
		"$.input.reviewers[0]..field", // double dot after index
		"$.input.reviewers[0].[0]",    // bracket after a dot
		// Adjacent empty / bracket-chain / double-dot variants.
		"$.input.reviewers[][0]",     // empty bracket then index
		"$.input.reviewers[0][1][2]", // bracket chain
		"$.input.reviewers..field",   // double dot
		"$.input.reviewers[0]field",  // field directly after index (no dot)
	}
	for _, path := range paths {
		t.Run(path, func(t *testing.T) {
			if inputDiagnosticPathValid(path) {
				t.Fatalf("malformed input path %q must fail the segment-structural grammar", path)
			}
		})
	}
}

// TestHTTPBoundaryReviewedFieldsStillPropagate proves a fully reviewed error
// (reviewed code, stage, path, hint, diagnostics) propagates all public fields
// through the real HTTP boundary.
func TestHTTPBoundaryReviewedFieldsStillPropagate(t *testing.T) {
	diagnostics := []map[string]any{
		{"severity": "error", "code": "input.unknown_field", "path": "$.input.reviewers[0]", "field": "reviewers", "message": "unknown input field"},
	}
	raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diagnostics)
	rawObj := map[string]any{}
	_ = json.Unmarshal([]byte(raw), &rawObj)
	rawObj["hint"] = "reviewed hint"
	encoded, _ := json.Marshal(rawObj)
	server := httpErrorServer(t, http.StatusUnprocessableEntity, string(encoded))
	remote := workflowRemote{client: httpErrorClient(server)}
	err := runHTTPWorkflowStep(t, remote)
	classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
	if classified.ServerCode != "invocation_input_invalid" {
		t.Fatalf("ServerCode = %q, want invocation_input_invalid", classified.ServerCode)
	}
	if classified.Stage != "invocation" {
		t.Fatalf("Stage = %q, want invocation", classified.Stage)
	}
	if classified.Path != "callback.workItems[2].key" {
		t.Fatalf("Path = %q", classified.Path)
	}
	if classified.Hint != "reviewed hint" {
		t.Fatalf("Hint = %q, want reviewed hint", classified.Hint)
	}
	if len(classified.Diagnostics) != 1 || classified.Diagnostics[0].Field != "reviewers" {
		t.Fatalf("reviewed diagnostic not preserved: %+v", classified.Diagnostics)
	}
}

// TestHTTPBoundaryStandardInputPathsStillAccepted proves the tightened grammar
// still accepts the resolved nested field/index paths the producer emits.
func TestHTTPBoundaryStandardInputPathsStillAccepted(t *testing.T) {
	paths := []string{
		"$.input",
		"$.input.reviewers",
		"$.input.reviewers[0]",
		"$.input.reviewers[0].name",
		"$.input.reviewers[0].name[1]",
		"$.input.a.b.c",
		"$.input.a[0].b[1].c",
		"$.input.items[10].value",
		"$.input.items[0]",
		"$.input.body",
	}
	for _, path := range paths {
		if !inputDiagnosticPathValid(path) {
			t.Fatalf("resolved input path %q must be accepted by the segment-structural grammar", path)
		}
	}
}

// TestHTTPBoundaryMalformedInputPathCannotUnlockPublicContent proves a malformed
// allowlisted diagnostic path (leading-zero index, trailing dot after index,
// bracket-after-dot, double-dot, empty bracket, bracket chain) never unlocks
// the public invocation-input problem; the generic redacted problem is kept.
func TestHTTPBoundaryMalformedInputPathCannotUnlockPublicContent(t *testing.T) {
	malformedPaths := []string{
		"$.input.reviewers[00]",
		"$.input.reviewers[01]",
		"$.input.reviewers[0].",
		"$.input.reviewers[0].[1]",
		"$.input.reviewers[0]..field",
		"$.input.reviewers[0][1]",
		"$.input.reviewers[][0]",
		"$.input.reviewers[0]field",
	}
	for _, path := range malformedPaths {
		t.Run(path, func(t *testing.T) {
			diag := []map[string]any{
				{"severity": "error", "code": "input.unknown_field", "path": path, "field": "reviewers", "message": "unlock-me"},
			}
			raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diag)
			server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
			remote := workflowRemote{client: httpErrorClient(server)}
			err := runHTTPWorkflowStep(t, remote)
			classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
			// The malformed path must not unlock the public problem with the
			// diagnostic path/message. The trusted code keeps its server-approved
			// bounded message (not the diagnostic-derived text).
			if strings.Contains(classified.Problem, path) {
				t.Fatalf("malformed path %q unlocked the public problem: %q", path, classified.Problem)
			}
			if strings.Contains(classified.Problem, "unlock-me") {
				t.Fatalf("malformed path %q unlocked the diagnostic message: %q", path, classified.Problem)
			}
			if strings.Contains(classified.Problem, "workflow Run input invalid") {
				t.Fatalf("malformed path %q unlocked the invocation-input diagnostic: %q", path, classified.Problem)
			}
		})
	}
}

// TestHTTPBoundaryStandardInputPathUnlocksPublicContent proves a resolved
// nested field/index path still unlocks the public invocation-input problem.
func TestHTTPBoundaryStandardInputPathUnlocksPublicContent(t *testing.T) {
	diag := []map[string]any{
		{"severity": "error", "code": "input.unknown_field", "path": "$.input.reviewers[0].name", "field": "reviewers", "message": "unknown input field"},
	}
	raw := rawWorkflowErrorBody("invocation_input_invalid", "invocation", "callback.workItems[2].key", diag)
	server := httpErrorServer(t, http.StatusUnprocessableEntity, raw)
	remote := workflowRemote{client: httpErrorClient(server)}
	err := runHTTPWorkflowStep(t, remote)
	classified := classifyWorkflowTransportError(t, err, "workflow step complete failed")
	if !strings.Contains(classified.Problem, "$.input.reviewers[0].name") {
		t.Fatalf("resolved input path must unlock the public problem, got %q", classified.Problem)
	}
	if !strings.Contains(classified.Problem, "unknown input field") {
		t.Fatalf("resolved input path must unlock the diagnostic message, got %q", classified.Problem)
	}
}
