package dialecttransport_test

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
	"github.com/pingcap-inc/nexuscrm-api-host/dialecttransport"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	apihostmiddleware "github.com/pingcap-inc/nexuscrm-api-host/middleware"
)

const testPermViewSetup dialect.Permission = "ViewSetup"

// testDialect is an in-test gin-free dialect implementing dialect.Dialect
// with a flexible field set so each test can vary one facet (projector, gate, ...).
type testDialect struct {
	name                   string
	pack                   string
	base                   string
	vers                   []string
	proj                   dialect.Projector
	internal               dialect.InternalErrorProjector
	disableDefaultInternal bool
	manifest               []dialect.Route
}

func (d testDialect) Name() string                      { return d.name }
func (d testDialect) OwningPack() string                { return d.pack }
func (d testDialect) BasePath() string                  { return d.base }
func (d testDialect) Versions() []string                { return d.vers }
func (d testDialect) ErrorProjector() dialect.Projector { return d.proj }
func (d testDialect) InternalErrorProjector() dialect.InternalErrorProjector {
	if d.internal != nil || d.disableDefaultInternal {
		return d.internal
	}
	return testInternalErrorProjector
}
func (d testDialect) Manifest() []dialect.Route { return d.manifest }

type testContracts struct {
	Records any
}

type stubRecords struct{}

func notFoundProjector(_ *dialect.Request, err *dialect.DomainWireError) (int, any, bool) {
	status := http.StatusBadRequest
	if err.Kind == dialect.WireNotFound {
		status = http.StatusNotFound
	}
	return status, gin.H{"errorKind": string(err.Kind), "message": err.Message}, true
}

func testInternalErrorProjector(req *dialect.Request) (int, any) {
	return http.StatusInternalServerError, gin.H{
		"dialectError": "INTERNAL",
		"version":      req.Version,
	}
}

// fullDialect exercises the three handler shapes: unguarded + version echo, a
// permission-gated user-aware route, and an error-projecting route.
func fullDialect() testDialect {
	return testDialect{
		name: "test-dialect", pack: "test-pack", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{Method: http.MethodGet, Path: "/ping", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(_ context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
					contracts, _ := deps.Contracts.(testContracts)
					return &dialect.Response{Status: http.StatusOK, Body: gin.H{
						"version":      req.Version,
						"recordsWired": contracts.Records != nil,
					}}, nil
				}},
			{Method: http.MethodGet, Path: "/whoami", Gate: dialect.Gate{Mode: dialect.GateAll, Permissions: []dialect.Permission{testPermViewSetup}},
				Handler: func(_ context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
					if req.User == nil {
						return &dialect.Response{Status: http.StatusUnauthorized, Body: gin.H{"error": "no user"}}, nil
					}
					return &dialect.Response{Status: http.StatusOK, Body: gin.H{"userId": req.User.ID}}, nil
				}},
			{Method: http.MethodGet, Path: "/boom", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(_ context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
					return nil, &dialect.DomainWireError{Kind: dialect.WireNotFound, Message: "gone"}
				}},
		},
	}
}

const (
	testUserKey              = "dialecttransport_test_user"
	testSystemPermissionsKey = "dialecttransport_test_system_permissions"
	testRateLimitSubjectKey  = "dialecttransport_test_rate_limit_subject"
)

// base middleware standing in for the host stack: sets the neutral auth user +
// loaded system permissions exactly as auth + PermissionLoader would, so the
// binder's permission gate runs against a realistic context without importing
// backend-owned packages.
func base(perms ...dialect.Permission) []gin.HandlerFunc {
	pm := map[string]bool{}
	for _, p := range perms {
		pm[string(p)] = true
	}
	return []gin.HandlerFunc{func(c *gin.Context) {
		c.Set(testUserKey, hostruntime.UserSession{ID: "005EXT0000000001", Name: "Ext User"})
		c.Set(testSystemPermissionsKey, pm)
		c.Next()
	}}
}

type testPermissionSessionContext struct{}

func (testPermissionSessionContext) ReadSession(c *gin.Context) apihostmiddleware.SessionLookup {
	user := testUserFromContext(c)
	if user == nil {
		return apihostmiddleware.SessionLookup{}
	}
	return apihostmiddleware.SessionLookup{
		User:   hostruntime.UserSession{ID: user.ID, Name: user.Name},
		Exists: true,
		Valid:  true,
	}
}

func (testPermissionSessionContext) WriteCurrentSession(*gin.Context, hostruntime.UserSession) {}

func (testPermissionSessionContext) WritePermissions(*gin.Context, map[string]bool, map[string]bool) {
}

func (testPermissionSessionContext) SystemPermissions(c *gin.Context) apihostmiddleware.PermissionLookup {
	permsInterface, exists := c.Get(testSystemPermissionsKey)
	if !exists {
		return apihostmiddleware.PermissionLookup{}
	}
	perms, ok := permsInterface.(map[string]bool)
	return apihostmiddleware.PermissionLookup{Permissions: perms, Exists: true, Valid: ok}
}

func (testPermissionSessionContext) RequestID(*gin.Context) string { return "" }

func testUserFromContext(c *gin.Context) *hostruntime.UserSession {
	if c == nil {
		return nil
	}
	userInterface, exists := c.Get(testUserKey)
	if !exists {
		return nil
	}
	user, ok := userInterface.(hostruntime.UserSession)
	if !ok {
		return nil
	}
	return &user
}

func testRateLimitSubjectFromContext(c *gin.Context) string {
	if c == nil {
		return ""
	}
	subject, _ := c.Get(testRateLimitSubjectKey)
	subjectID, _ := subject.(string)
	return strings.TrimSpace(subjectID)
}

func mount(
	router *gin.Engine,
	dialects []dialect.Dialect,
	deps dialect.Deps,
	base []gin.HandlerFunc,
	userFromCtx dialecttransport.UserExtractor,
	permKnown dialecttransport.PermissionResolver,
	packCatalog map[string]bool,
	opts ...dialecttransport.MountOption,
) []dialect.Mount {
	opts = append([]dialecttransport.MountOption{dialecttransport.WithPermissionSessionContext(testPermissionSessionContext{})}, opts...)
	return dialecttransport.Mount(router, dialects, deps, base, userFromCtx, permKnown, packCatalog, opts...)
}

func mountRegistered(
	router *gin.Engine,
	deps dialect.Deps,
	base []gin.HandlerFunc,
	userFromCtx dialecttransport.UserExtractor,
	permKnown dialecttransport.PermissionResolver,
	packCatalog map[string]bool,
	opts ...dialecttransport.MountOption,
) []dialect.Mount {
	opts = append([]dialecttransport.MountOption{dialecttransport.WithPermissionSessionContext(testPermissionSessionContext{})}, opts...)
	return dialecttransport.MountRegistered(router, deps, base, userFromCtx, permKnown, packCatalog, opts...)
}

func deps() dialect.Deps { return dialect.Deps{Contracts: testContracts{Records: stubRecords{}}} }

// permsKnown stands in for the host's permission catalog resolver
// (PermissionService.ListSystemPermissions): it reports a permission as provided iff
// it is in the given set. This is the EXISTENCE question (does the host model this
// capability), distinct from base(...)'s per-request GRANT question (does this user
// hold it) — an unknown/typo'd gate permission must fail the mount closed regardless
// of who is calling.
func permsKnown(perms ...dialect.Permission) dialecttransport.PermissionResolver {
	set := map[dialect.Permission]bool{}
	for _, p := range perms {
		set[p] = true
	}
	return func(p dialect.Permission) bool { return set[p] }
}

func serve(router *gin.Engine, target string) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
	return w
}

func TestBinderMountsManifestServesVersionAndDepsAndUser(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	mounts := mount(router, []dialect.Dialect{fullDialect()}, deps(),
		base(testPermViewSetup), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{"test-pack": true})

	// Unified Mount records: shared location facts for host-owned discovery.
	if len(mounts) != 1 || mounts[0].Dialect != "test-dialect" || mounts[0].Path != "/svc/v1" ||
		mounts[0].BasePath != "/svc" || mounts[0].Version != "v1" {
		t.Fatalf("mounts = %#v", mounts)
	}

	// /ping: version param flows in, contract Deps reach the handler.
	w := serve(router, "/svc/v1/ping")
	body := decode(t, w)
	if w.Code != 200 || body["version"] != "v1" || body["recordsWired"] != true {
		t.Fatalf("/ping = %d %#v", w.Code, body)
	}

	// /whoami: gate passes (perm granted) and the user is read via testUserFromContext.
	w = serve(router, "/svc/v1/whoami")
	body = decode(t, w)
	if w.Code != 200 || body["userId"] != "005EXT0000000001" {
		t.Fatalf("/whoami = %d %#v", w.Code, body)
	}
}

func TestBinderThreadsAuthoritativeRateLimitSubject(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	d := testDialect{
		name: "limits-dialect", pack: "test-pack", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{Method: http.MethodGet, Path: "/limits", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(_ context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
					return &dialect.Response{Status: http.StatusOK, Body: gin.H{
						"rateLimitSubjectID": req.RateLimitSubjectID,
					}}, nil
				}},
		},
	}
	baseWithRateLimitSubject := []gin.HandlerFunc{func(c *gin.Context) {
		c.Set(testUserKey, hostruntime.UserSession{ID: "005EXT0000000001", Name: "Ext User"})
		c.Set(testRateLimitSubjectKey, "app-1")
		c.Next()
	}}
	mount(router, []dialect.Dialect{d}, deps(),
		baseWithRateLimitSubject, testUserFromContext, permsKnown(), map[string]bool{"test-pack": true},
		dialecttransport.WithRateLimitSubjectExtractor(testRateLimitSubjectFromContext))

	w := serve(router, "/svc/v1/limits")
	body := decode(t, w)
	if w.Code != http.StatusOK || body["rateLimitSubjectID"] != "app-1" {
		t.Fatalf("/limits = %d %#v, want rateLimitSubjectID app-1", w.Code, body)
	}
}

func TestBinderDispatchReentersMountedRouterThroughNeutralDeps(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	cacheKey := struct{}{}
	d := testDialect{
		name: "dispatch-dialect", pack: "test-pack", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{Method: http.MethodPost, Path: "/call", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(ctx context.Context, _ *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
					if deps.Dispatch == nil {
						return &dialect.Response{Status: http.StatusInternalServerError, Body: gin.H{"error": "missing dispatch"}}, nil
					}
					parentCtxWithRequestScopedValue := context.WithValue(ctx, cacheKey, "parent-cache")
					resp, err := deps.Dispatch(parentCtxWithRequestScopedValue, dialect.DispatchRequest{
						Method: http.MethodGet,
						Path:   "/svc/v1/target",
						Header: map[string]string{"X-Composite": "yes"},
					})
					if err != nil {
						return nil, err
					}
					return &dialect.Response{Status: http.StatusOK, Body: gin.H{
						"subStatus": resp.Status,
						"subBody":   string(resp.Body),
						"subHeader": resp.Header["X-Target"],
					}}, nil
				}},
			{Method: http.MethodGet, Path: "/target", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(ctx context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
					if req.User == nil {
						return &dialect.Response{Status: http.StatusUnauthorized, Body: gin.H{"error": "no user"}}, nil
					}
					if got := ctx.Value(cacheKey); got != nil {
						return &dialect.Response{Status: http.StatusConflict, Body: gin.H{"leakedContextValue": got}}, nil
					}
					return &dialect.Response{
						Status:  http.StatusAccepted,
						Headers: map[string]string{"X-Target": "reached"},
						Body: gin.H{
							"version":    req.Version,
							"userId":     req.User.ID,
							"xComposite": req.Header["X-Composite"][0],
						},
					}, nil
				}},
		},
	}
	mount(router, []dialect.Dialect{d}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})

	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/svc/v1/call", nil))
	if w.Code != http.StatusOK {
		t.Fatalf("/call = %d body=%s", w.Code, w.Body.String())
	}
	body := decode(t, w)
	if body["subStatus"] != float64(http.StatusAccepted) {
		t.Fatalf("subStatus = %#v, want %d; body=%#v", body["subStatus"], http.StatusAccepted, body)
	}
	if body["subHeader"] != "reached" {
		t.Fatalf("subHeader = %#v; body=%#v", body["subHeader"], body)
	}
	var sub map[string]interface{}
	if err := json.Unmarshal([]byte(body["subBody"].(string)), &sub); err != nil {
		t.Fatalf("subBody is not JSON: %v body=%s", err, body["subBody"])
	}
	if sub["version"] != "v1" || sub["userId"] != "005EXT0000000001" || sub["xComposite"] != "yes" {
		t.Fatalf("subBody = %#v", sub)
	}
}

func TestBinderGateDeniesWithoutPermission(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	mount(router, []dialect.Dialect{fullDialect()}, deps(),
		base(), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{"test-pack": true}) // perm exists in catalog; user just isn't granted it

	w := serve(router, "/svc/v1/whoami")
	if w.Code != http.StatusForbidden {
		t.Fatalf("gated route without permission = %d, want 403; body=%s", w.Code, w.Body.String())
	}
}

func TestBinderMountsUngatedRouteWithoutPermissionGateProvider(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	d := testDialect{
		name: "ungated", pack: "test-pack", base: "/svc", vers: []string{"v1"},
		proj: notFoundProjector,
		manifest: []dialect.Route{
			{Method: http.MethodGet, Path: "/ping", Gate: dialect.Gate{Mode: dialect.GateNone}, Handler: okHandler},
		},
	}

	dialecttransport.Mount(router, []dialect.Dialect{d}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})

	if w := serve(router, "/svc/v1/ping"); w.Code != http.StatusOK {
		t.Fatalf("ungated route without permission gate provider = %d, want 200", w.Code)
	}
}

func TestBinderFailsClosedOnGatedRouteWithoutPermissionGateProvider(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	defer func() {
		if recovered := recover(); recovered == nil {
			t.Fatal("Mount did not fail closed for a gated route without permission gate provider")
		}
	}()
	dialecttransport.Mount(router, []dialect.Dialect{fullDialect()}, deps(),
		base(testPermViewSetup), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{"test-pack": true})
}

func TestBinderProjectsDomainWireErrorGinFree(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	mount(router, []dialect.Dialect{fullDialect()}, deps(),
		base(testPermViewSetup), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{"test-pack": true})

	w := serve(router, "/svc/v1/boom")
	body := decode(t, w)
	if w.Code != http.StatusNotFound || body["errorKind"] != string(dialect.WireNotFound) {
		t.Fatalf("/boom = %d %#v, want 404 projected via the dialect's gin-free projector", w.Code, body)
	}
}

func TestBinderSkipsInactivePack(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	mounts := mount(router, []dialect.Dialect{fullDialect()}, deps(),
		base(testPermViewSetup), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{"test-pack": false})

	if len(mounts) != 0 {
		t.Fatalf("inactive pack mounted: %#v", mounts)
	}
	if w := serve(router, "/svc/v1/ping"); w.Code != http.StatusNotFound {
		t.Fatalf("inactive-pack route = %d, want 404", w.Code)
	}
}

// TestBinderSkipsAbsentOwningPack pins the settled 3-state rule (acceptance #3):
// an OwningPack ABSENT from the host pack catalog (an empty catalog here) is treated
// as a not-yet-installed source-backed app pack and is SKIPPED — Mount returns
// WITHOUT panic and mounts zero routes. This REPLACES the old 'unknown pack'->panic
// case: the binder no longer fails closed on pack absence.
//
// DOCUMENTED SALESFORCE-BOUNDARY DELTA: an app-pack OwningPack typo therefore fails
// OPEN to skip — the dialect's whole route surface silently vanishes at boot until
// that pack installs, with no panic. The binary has no authoritative source-backed
// name list to validate an app-pack OwningPack against; enumerating one would be an
// L1-in-L0 leak and a second SSoT competing with the _System_InstalledPack row. This
// is the identical cost the prior circular packCatalogFor self-seed already accepted.
func TestBinderSkipsAbsentOwningPack(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	d := fullDialect()
	d.pack = "no-such-pack" // absent from the empty catalog below
	mounts := mount(router, []dialect.Dialect{d}, deps(),
		base(testPermViewSetup), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{})

	if len(mounts) != 0 {
		t.Fatalf("absent-OwningPack dialect mounted: %#v", mounts)
	}
	if w := serve(router, "/svc/v1/ping"); w.Code != http.StatusNotFound {
		t.Fatalf("absent-pack route = %d, want 404 (skipped, no panic)", w.Code)
	}
}

func TestBinderFailsClosed(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cases := map[string][]dialect.Dialect{
		"nil projector": {func() testDialect {
			d := fullDialect()
			d.pack = "" // always active so the fail-closed check fires
			d.proj = nil
			return d
		}()},
		"nil internal projector": {func() testDialect {
			d := fullDialect()
			d.pack = "" // always active so the fail-closed check fires
			d.internal = nil
			d.disableDefaultInternal = true
			return d
		}()},
		"empty-perm gate": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", Gate: dialect.Gate{Mode: dialect.GateAll}, Handler: okHandler}}
			return d
		}()},
		"unknown gate mode": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", Gate: dialect.Gate{Mode: dialect.GateMode(99)}, Handler: okHandler}}
			return d
		}()},
		// Silent fail-open: permissions declared but Mode left at the zero value
		// (GateNone) must NOT mount the route ungated.
		"permissions under GateNone": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", Gate: dialect.Gate{Permissions: []dialect.Permission{testPermViewSetup}}, Handler: okHandler}}
			return d
		}()},
		"nil handler": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", Gate: dialect.Gate{Mode: dialect.GateNone}, Handler: nil}}
			return d
		}()},
		"invalid method": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: "", Path: "/x", Handler: okHandler}}
			return d
		}()},
		"non-absolute path": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "x", Handler: okHandler}}
			return d
		}()},
		// Traversal segment would let gin's path.Clean escape the mount root.
		"traversal path": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/../../api/debug", Handler: okHandler}}
			return d
		}()},
		"duplicate route": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{
				{Method: http.MethodGet, Path: "/dup", Handler: okHandler},
				{Method: http.MethodGet, Path: "/dup", Handler: okHandler},
			}
			return d
		}()},
		"empty dialect name": {
			testDialect{name: "", base: "/a", vers: []string{"v1"}, proj: notFoundProjector, manifest: []dialect.Route{{Method: http.MethodGet, Path: "/p", Handler: okHandler}}},
		},
		// Distinct BasePaths (no path collision) — isolates the Name uniqueness key.
		"duplicate dialect name": {
			testDialect{name: "dup", base: "/a", vers: []string{"v1"}, proj: notFoundProjector, manifest: []dialect.Route{{Method: http.MethodGet, Path: "/p", Handler: okHandler}}},
			testDialect{name: "dup", base: "/b", vers: []string{"v1"}, proj: notFoundProjector, manifest: []dialect.Route{{Method: http.MethodGet, Path: "/q", Handler: okHandler}}},
		},
		"path collision": {
			testDialect{name: "a", base: "/svc", vers: []string{"v1"}, proj: notFoundProjector, manifest: []dialect.Route{{Method: http.MethodGet, Path: "/p", Handler: okHandler}}},
			testDialect{name: "b", base: "/svc", vers: []string{"v1"}, proj: notFoundProjector, manifest: []dialect.Route{{Method: http.MethodGet, Path: "/q", Handler: okHandler}}},
		},
		// Dangling host-capability reference (same class as the pack case below): a gate
		// permission the host catalog does not provide. permsKnown(PermViewSetup) below
		// does NOT know "ViewSetp", so the typo'd capability must fail the mount closed
		// rather than mount a route only superusers could reach.
		"unknown permission": {func() testDialect {
			d := fullDialect()
			d.pack = "" // always active so the gate check (not the pack check) fires
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", Gate: dialect.Gate{Mode: dialect.GateAll, Permissions: []dialect.Permission{"ViewSetp"}}, Handler: okHandler}}
			return d
		}()},
		"negative buffered body limit": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodPost, Path: "/x", BodyLimitBytes: -1, Handler: okHandler}}
			return d
		}()},
		"oversized buffered body limit": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodPost, Path: "/x", BodyLimitBytes: 26 << 20, Handler: okHandler}}
			return d
		}()},
		"stream body without limit": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodPost, Path: "/x", BodyMode: dialect.BodyStream, Handler: okHandler}}
			return d
		}()},
		"stream body on bodyless method": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodGet, Path: "/x", BodyMode: dialect.BodyStream, BodyLimitBytes: 1, Handler: okHandler}}
			return d
		}()},
		"unknown body mode": {func() testDialect {
			d := fullDialect()
			d.pack = ""
			d.manifest = []dialect.Route{{Method: http.MethodPost, Path: "/x", BodyMode: dialect.RequestBodyMode(99), Handler: okHandler}}
			return d
		}()},
	}
	for name, ds := range cases {
		t.Run(name, func(t *testing.T) {
			defer func() {
				if recover() == nil {
					t.Fatalf("Mount did not fail closed for %q", name)
				}
			}()
			mount(gin.New(), ds, deps(), base(), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{})
		})
	}
}

func TestBinderStreamsRequestBodyWithDeclaredLimit(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "upload", base: "/u", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodPost, Path: "/file", BodyMode: dialect.BodyStream, BodyLimitBytes: 16, Handler: func(_ context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			if req.Body != nil {
				t.Fatalf("stream route received buffered Body = %q, want nil", string(req.Body))
			}
			if req.BodyStream == nil {
				t.Fatal("stream route received nil BodyStream")
			}
			got, err := io.ReadAll(req.BodyStream)
			if err != nil {
				t.Fatalf("read BodyStream: %v", err)
			}
			return &dialect.Response{Status: http.StatusCreated, Body: gin.H{
				"body":          string(got),
				"contentLength": req.ContentLength,
			}}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/u/v1/file", strings.NewReader("stream me")))
	body := decode(t, w)
	if w.Code != http.StatusCreated || body["body"] != "stream me" || body["contentLength"] != float64(len("stream me")) {
		t.Fatalf("stream upload = %d %#v", w.Code, body)
	}
}

func TestBinderStreamRequestBodyLimitIsApplied(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "upload", base: "/u", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodPost, Path: "/file", BodyMode: dialect.BodyStream, BodyLimitBytes: 3, Handler: func(_ context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			_, err := io.ReadAll(req.BodyStream)
			if err == nil {
				return &dialect.Response{Status: http.StatusOK, Body: gin.H{"limited": false}}, nil
			}
			if !dialect.IsBodyStreamTooLarge(err) {
				t.Fatalf("stream limit error = %T %[1]v, want BodyStreamTooLarge", err)
			}
			return &dialect.Response{Status: http.StatusRequestEntityTooLarge, Body: gin.H{
				"limited": true,
			}}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/u/v1/file", strings.NewReader("too large"))
	req.ContentLength = -1 // unknown length: the cap is enforced as the handler drains BodyStream.
	router.ServeHTTP(w, req)
	body := decode(t, w)
	if w.Code != http.StatusRequestEntityTooLarge || body["limited"] != true {
		t.Fatalf("stream limit = %d %#v", w.Code, body)
	}
}

func TestBinderStreamRequestKnownOversizeIsRejectedBeforeHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	called := false
	d := testDialect{
		name: "upload", base: "/u", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodPost, Path: "/file", BodyMode: dialect.BodyStream, BodyLimitBytes: 3, Handler: func(_ context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			called = true
			_, _ = io.ReadAll(req.BodyStream)
			return &dialect.Response{Status: http.StatusOK, Body: gin.H{"reached": true}}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/u/v1/file", strings.NewReader("too large")))
	body := decode(t, w)
	if w.Code != http.StatusBadRequest || body["errorKind"] != string(dialect.WireRequestTooLarge) {
		t.Fatalf("known oversize stream request = %d %#v, want RequestTooLarge", w.Code, body)
	}
	if called {
		t.Fatal("handler ran for a known-oversize stream request")
	}
}

func TestBinderStreamRequestReadFailureIsNeutral(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "upload", base: "/u", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodPost, Path: "/file", BodyMode: dialect.BodyStream, BodyLimitBytes: 16, Handler: func(_ context.Context, req *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			_, err := io.ReadAll(req.BodyStream)
			if err == nil {
				return &dialect.Response{Status: http.StatusOK, Body: gin.H{"readFailed": false}}, nil
			}
			if !dialect.IsBodyStreamReadFailed(err) {
				t.Fatalf("stream read error = %T %[1]v, want BodyStreamReadFailed", err)
			}
			return &dialect.Response{Status: http.StatusBadRequest, Body: gin.H{"readFailed": true}}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/u/v1/file", errBody{}))
	body := decode(t, w)
	if w.Code != http.StatusBadRequest || body["readFailed"] != true {
		t.Fatalf("stream read failure = %d %#v", w.Code, body)
	}
}

func TestBinderFiltersReservedRefreshedTokenHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "headers", base: "/h", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/record", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusOK,
				Headers: map[string]string{
					"x-refreshed-token": "spoofed-token",
					"X-Trace":           "buffered",
				},
				Body: gin.H{"ok": true},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/h/v1/record")
	if got := w.Header().Get(apihostmiddleware.HeaderRefreshedToken); got != "" {
		t.Fatalf("%s = %q, want empty", apihostmiddleware.HeaderRefreshedToken, got)
	}
	if got := w.Header().Get("X-Trace"); got != "buffered" {
		t.Fatalf("X-Trace = %q, want buffered", got)
	}
}

type trackingReadCloser struct {
	*strings.Reader
	closed bool
}

func (r *trackingReadCloser) Close() error {
	r.closed = true
	return nil
}

func TestBinderWritesRawStreamResponse(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw-bytes")}
	contentLength := int64(len("raw-bytes"))
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusAccepted,
				Headers: map[string]string{
					"Content-Disposition": `attachment; filename="file.bin"`,
					"X-Trace":             "stream",
					"x-refreshed-token":   "spoofed-token",
				},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentType:   "application/octet-stream",
					ContentLength: &contentLength,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusAccepted {
		t.Fatalf("stream status = %d, want 202", w.Code)
	}
	if got := w.Body.String(); got != "raw-bytes" {
		t.Fatalf("stream body = %q, want raw bytes without JSON encoding", got)
	}
	if ct := w.Header().Get("Content-Type"); ct != "application/octet-stream" {
		t.Fatalf("Content-Type = %q, want application/octet-stream", ct)
	}
	if cl := w.Header().Get("Content-Length"); cl != "9" {
		t.Fatalf("Content-Length = %q, want 9", cl)
	}
	if cd := w.Header().Get("Content-Disposition"); cd != `attachment; filename="file.bin"` {
		t.Fatalf("Content-Disposition = %q", cd)
	}
	if got := w.Header().Get(apihostmiddleware.HeaderRefreshedToken); got != "" {
		t.Fatalf("%s = %q, want empty", apihostmiddleware.HeaderRefreshedToken, got)
	}
	if !stream.closed {
		t.Fatal("stream reader was not closed by the binder")
	}
}

func TestBinderFailsClosedOnStreamContentLengthTooSmall(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw-bytes")}
	declared := int64(3)
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status:  http.StatusOK,
				Headers: map[string]string{"X-Should-Not-Leak": "yes"},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentType:   "application/octet-stream",
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("too-small stream length status = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Body.String(); got == "raw" || got == "raw-bytes" {
		t.Fatalf("too-small stream length emitted partial raw body %q", got)
	}
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("too-small stream length leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("too-small stream length did not close the reader")
	}
}

func TestBinderFailsClosedOnStreamContentLengthTooLarge(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	declared := int64(9)
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status:  http.StatusOK,
				Headers: map[string]string{"X-Should-Not-Leak": "yes"},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("too-large stream length status = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("too-large stream length leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("too-large stream length did not close the reader")
	}
}

func TestBinderFailsClosedOnOversizedStreamContentLength(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("")}
	declared := int64(26 << 20)
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status:  http.StatusOK,
				Headers: map[string]string{"X-Should-Not-Leak": "yes"},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("oversized stream length status = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("oversized stream length leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("oversized stream length did not close the reader")
	}
}

func TestBinderFailsClosedOnUnknownLengthStream(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("unknown-length")}
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusOK,
				Stream: &dialect.ResponseStream{
					Reader:      stream,
					ContentType: "application/octet-stream",
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("unknown-length stream status = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("Content-Length"); got != "" {
		t.Fatalf("unknown-length stream emitted Content-Length %q, want none", got)
	}
	if !stream.closed {
		t.Fatal("unknown-length stream did not close the reader")
	}
}

func TestBinderFailsClosedOnInvalidStreamResponse(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status:  http.StatusOK,
				Body:    gin.H{"shape": "json"},
				Headers: map[string]string{"X-Should-Not-Leak": "yes"},
				Stream:  &dialect.ResponseStream{Reader: stream},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("mixed stream/json response = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("invalid stream response leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("invalid stream response did not close the reader")
	}
}

func TestBinderFailsClosedOnNegativeStreamContentLength(t *testing.T) {
	gin.SetMode(gin.TestMode)
	negative := int64(-1)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status:  http.StatusOK,
				Headers: map[string]string{"X-Should-Not-Leak": "yes"},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentLength: &negative,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("negative stream length response = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("negative stream length leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("negative stream length response did not close the reader")
	}
}

func TestBinderFailsClosedOnStreamContentLengthHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	declared := int64(len("raw"))
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusOK,
				Headers: map[string]string{
					"Content-Length":    "999",
					"X-Should-Not-Leak": "yes",
				},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("stream Content-Length header response = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("Content-Length"); got != "" {
		t.Fatalf("stream Content-Length header leaked %q", got)
	}
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("stream Content-Length header leaked dialect header %q", got)
	}
	if !stream.closed {
		t.Fatal("stream Content-Length header response did not close the reader")
	}
}

func TestBinderFailsClosedOnStreamContentTypeHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	declared := int64(len("raw"))
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusOK,
				Headers: map[string]string{
					"Content-Type":      "text/plain",
					"X-Should-Not-Leak": "yes",
				},
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentType:   "application/octet-stream",
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("stream Content-Type header response = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Header().Get("X-Should-Not-Leak"); got != "" {
		t.Fatalf("stream Content-Type header leaked dialect header %q", got)
	}
	if got := w.Body.String(); got == "raw" {
		t.Fatalf("stream Content-Type header emitted raw body %q", got)
	}
	if !stream.closed {
		t.Fatal("stream Content-Type header response did not close the reader")
	}
}

func TestBinderFailsClosedOnMissingStreamContentType(t *testing.T) {
	gin.SetMode(gin.TestMode)
	stream := &trackingReadCloser{Reader: strings.NewReader("raw")}
	declared := int64(len("raw"))
	d := testDialect{
		name: "download", base: "/d", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/file", Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
			return &dialect.Response{
				Status: http.StatusOK,
				Stream: &dialect.ResponseStream{
					Reader:        stream,
					ContentLength: &declared,
				},
			}, nil
		}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(), map[string]bool{})

	w := serve(router, "/d/v1/file")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("missing stream Content-Type response = %d, want 500", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
	if got := w.Body.String(); got == "raw" {
		t.Fatalf("missing stream Content-Type emitted raw body %q", got)
	}
	if !stream.closed {
		t.Fatal("missing stream Content-Type response did not close the reader")
	}
}

func okHandler(context.Context, *dialect.Request, dialect.Deps) (*dialect.Response, error) {
	return &dialect.Response{Status: http.StatusOK}, nil
}

type errBody struct{}

func (errBody) Read([]byte) (int, error) { return 0, errors.New("body read boom") }
func (errBody) Close() error             { return nil }

type internalTestError struct {
	msg   string
	cause error
}

func (e internalTestError) Error() string {
	return e.msg + ": " + e.cause.Error()
}

func (e internalTestError) Unwrap() error { return e.cause }

func (e internalTestError) InternalCondition() bool { return true }

// TestBinderFailsClosedOnBodyReadError proves the transport boundary fails closed
// when the request body cannot be read (client disconnect, malformed chunked): the
// handler is NOT called with truncated input; the binder returns 400.
func TestBinderFailsClosedOnBodyReadError(t *testing.T) {
	gin.SetMode(gin.TestMode)
	called := false
	d := testDialect{
		name: "echo", base: "/e", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodPost, Path: "/echo", Gate: dialect.Gate{Mode: dialect.GateNone},
			Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
				called = true
				return &dialect.Response{Status: http.StatusOK}, nil
			}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{})

	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/e/v1/echo", errBody{}))
	if w.Code != http.StatusBadRequest {
		t.Fatalf("body read error = %d, want 400 (fail closed)", w.Code)
	}
	if called {
		t.Fatal("handler was called with a truncated/failed body read")
	}
}

// TestBinderInternalErrorWrappingWireErrorStaysInternal guards the gate invariant
// (mirrors GlobalErrorHandler's golden test): an internal error that wraps a
// DomainWireError as its cause — a tx-rollback / infra failure — must render as a
// 500, NOT be projected as a dialect 4xx, even though errors.As would find the
// wrapped wire error. Internal-first classification.
func TestBinderInternalErrorWrappingWireErrorStaysInternal(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "intl", base: "/i", vers: []string{"v1"}, proj: notFoundProjector,
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/boom",
			Handler: func(context.Context, *dialect.Request, dialect.Deps) (*dialect.Response, error) {
				return nil, internalTestError{
					msg:   "tx rollback failed",
					cause: &dialect.DomainWireError{Kind: dialect.WireNotFound, Message: "x"},
				}
			}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{})
	w := serve(router, "/i/v1/boom")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("internal error wrapping a wire error = %d, want 500 (must not project as a dialect 4xx)", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
}

// TestBinderFailsClosedWhenProjectorDeclines proves the restored decline signal: a
// projector that returns projected=false makes the binder emit the dialect-owned
// generic 500 rather than ship a wrong wire shape — the fail-closed semantics the
// gin-coupled projector's bool return had.
func TestBinderFailsClosedWhenProjectorDeclines(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d := testDialect{
		name: "decliner", base: "/d", vers: []string{"v1"},
		proj: func(*dialect.Request, *dialect.DomainWireError) (int, any, bool) { return 0, nil, false },
		manifest: []dialect.Route{{Method: http.MethodGet, Path: "/boom",
			Handler: func(context.Context, *dialect.Request, dialect.Deps) (*dialect.Response, error) {
				return nil, &dialect.DomainWireError{Kind: dialect.WireNotFound, Message: "x"}
			}}},
	}
	router := gin.New()
	mount(router, []dialect.Dialect{d}, deps(), base(), testUserFromContext, permsKnown(testPermViewSetup), map[string]bool{})
	w := serve(router, "/d/v1/boom")
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("declined projection = %d, want 500 (fail closed)", w.Code)
	}
	assertDialectInternalError(t, w, "v1")
}

func decode(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
	t.Helper()
	var body map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode: %v body=%s", err, w.Body.String())
	}
	return body
}

func assertDialectInternalError(t *testing.T, w *httptest.ResponseRecorder, version string) {
	t.Helper()
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("internal fallback status = %d, want 500; body=%s", w.Code, w.Body.String())
	}
	body := decode(t, w)
	if body["dialectError"] != "INTERNAL" || body["version"] != version {
		t.Fatalf("internal fallback body = %#v, want dialect-owned envelope for version %q", body, version)
	}
}

// bodyReadErrReader fails on the first Read, simulating a transport-level body
// failure (client disconnect / malformed chunked encoding / truncated upload).
type bodyReadErrReader struct{}

func (bodyReadErrReader) Read([]byte) (int, error) {
	return 0, errors.New("connection reset by peer")
}

// postBodyDialect is a body-bearing route (POST) so the binder's io.ReadAll path
// runs; the handler must NOT be reached on a body-read failure.
func postBodyDialect(proj dialect.Projector) testDialect {
	return testDialect{
		name: "post-dialect", pack: "test-pack", base: "/pb", vers: []string{"v1"},
		proj: proj,
		manifest: []dialect.Route{
			{Method: http.MethodPost, Path: "/create", Gate: dialect.Gate{Mode: dialect.GateNone},
				Handler: func(_ context.Context, _ *dialect.Request, _ dialect.Deps) (*dialect.Response, error) {
					return &dialect.Response{Status: http.StatusCreated, Body: gin.H{"reached": true}}, nil
				}},
		},
	}
}

func postErrReq(target string) *http.Request {
	return httptest.NewRequest(http.MethodPost, target, bodyReadErrReader{})
}

// TestBinderBodyReadFailureRoutesThroughProjector pins the L0->L1 seam fix: a
// transport-level body-read failure is rendered by the route's dialect projector
// (not a hardcoded generic envelope), so L1 owns the wire shape. When the dialect
// declines, the binder fails closed to the dialect-owned generic 500 — identical
// to the handler-error arm — never emitting an unknown wire shape and never
// reaching the handler.
func TestBinderBodyReadFailureRoutesThroughProjector(t *testing.T) {
	gin.SetMode(gin.TestMode)

	// Projected: the dialect renders the neutral WireMalformedRequest in its shape.
	router := gin.New()
	mount(router, []dialect.Dialect{postBodyDialect(notFoundProjector)}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})
	w := httptest.NewRecorder()
	router.ServeHTTP(w, postErrReq("/pb/v1/create"))
	if w.Code != http.StatusBadRequest {
		t.Fatalf("projected body-read failure status = %d, want 400; body=%s", w.Code, w.Body.String())
	}
	body := decode(t, w)
	if body["errorKind"] != string(dialect.WireMalformedRequest) || body["message"] != "Invalid request body" {
		t.Fatalf("projected body = %#v, want kind=MalformedRequest message=\"Invalid request body\"", body)
	}
	if body["reached"] != nil {
		t.Fatalf("handler must NOT run on a body-read failure: %#v", body)
	}

	// Declined: outward route fails closed to the dialect-owned generic 500, no
	// unknown wire shape.
	declined := func(*dialect.Request, *dialect.DomainWireError) (int, any, bool) { return 0, nil, false }
	router2 := gin.New()
	mount(router2, []dialect.Dialect{postBodyDialect(declined)}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})
	w2 := httptest.NewRecorder()
	router2.ServeHTTP(w2, postErrReq("/pb/v1/create"))
	if w2.Code != http.StatusInternalServerError {
		t.Fatalf("declined body-read failure status = %d, want 500 (fail closed)", w2.Code)
	}
	assertDialectInternalError(t, w2, "v1")
}

// infiniteBody yields bytes without end, so the binder's bounded read trips the cap.
type infiniteBody struct{}

func (infiniteBody) Read(p []byte) (int, error) {
	for i := range p {
		p[i] = 'x'
	}
	return len(p), nil
}

// TestBinderRejectsOversizeBody pins the bounded-read hardening: a body over the cap
// is refused via the projector seam (WireRequestTooLarge) without reaching the
// handler, and fails closed to the dialect-owned generic 500 if the dialect
// declines — the same seam as a read failure. (notFoundProjector renders
// non-NotFound kinds at 400; the v63 413 rendering is asserted by the CRM
// partition tests.)
func TestBinderRejectsOversizeBody(t *testing.T) {
	gin.SetMode(gin.TestMode)

	router := gin.New()
	mount(router, []dialect.Dialect{postBodyDialect(notFoundProjector)}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})
	w := httptest.NewRecorder()
	router.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/pb/v1/create", infiniteBody{}))
	if w.Code != http.StatusBadRequest {
		t.Fatalf("oversize status = %d, want 400 (projector rendering); body=%s", w.Code, w.Body.String())
	}
	body := decode(t, w)
	if body["errorKind"] != string(dialect.WireRequestTooLarge) {
		t.Fatalf("oversize body = %#v, want kind=RequestTooLarge", body)
	}
	if body["reached"] != nil {
		t.Fatalf("handler must NOT run on an oversize body: %#v", body)
	}

	declined := func(*dialect.Request, *dialect.DomainWireError) (int, any, bool) { return 0, nil, false }
	router2 := gin.New()
	mount(router2, []dialect.Dialect{postBodyDialect(declined)}, deps(),
		base(), testUserFromContext, permsKnown(), map[string]bool{"test-pack": true})
	w2 := httptest.NewRecorder()
	router2.ServeHTTP(w2, httptest.NewRequest(http.MethodPost, "/pb/v1/create", infiniteBody{}))
	if w2.Code != http.StatusInternalServerError {
		t.Fatalf("declined oversize status = %d, want 500 (fail closed)", w2.Code)
	}
	assertDialectInternalError(t, w2, "v1")
}
