// iteration-002 P1-T8：Phase 1（漏洞写面开放给 agent）集成验收（qa 关卡）。
//
// 覆盖 docs/tasks/iteration-002/002-phase1-backend-permission-tasks.md P1-T8 的 7 项验收点
// （plan docs/plan/002-vuln-write-openapi-mcp-plan.md §3 Phase 1 验收）：
//
//	① 白名单：6 条写路由按 method 放行（穿过白名单到达业务层）；越界 403
//	   （DELETE、白名单外 POST、白名单未开放的 Claim 路由）
//	② scope：写 scope 建 Key 成功且可用；denylist scope（vuln:manage_findings）建 Key 被拒；
//	   持 denylist scope 的 Key 请求时被 AgentAuth 拒
//	③ 权限求交：主人 DB 有 close_findings 但 Key 没有 → transition fixed 被业务层 403；Key 有 → 放行
//	④ 三条写防护：agent 改敏感字段 403、改时间字段 403、Create 的 submitter 强制 = Key 主人
//	⑤ 归因：agent 评论/提交后，评论作者与事件 actor_name 为 Key 名（非空）
//	⑥ 旧值：编辑普通字段后 details_updated 事件 meta 含 previous_values；敏感字段旧值不外泄
//	⑦ 回归：人类 JWT 走原有写接口行为不变（Create 指定 submitter/时间、改敏感/时间字段、
//	   白名单不拦截人类）
//	⑧ 提交门槛（iteration-003 P1-T2）：view-only（agent Key 或人类 JWT）提交漏洞
//	   被 Permission 中间件 403（vuln:submit_findings 子组）
//
// 基建（复用迭代 001 phase1_integration_test.go 的进程内套路）：
// sqlite in-memory（shared cache）+ 真实中间件链
// CombinedAuth(AgentAuth, Auth) → AgentRouteAllowlist → AuditTrail → Permission(路由级)
// + 真实 vuln Handler/Service/Gorm 仓储（Vulnerability/Event/Comment，sqlite）
// + 真实 APIKey/Audit Gorm 仓储 + 真实 agent 建 Key handler。
//
// Mock/替代说明（均为外部系统边界，不削弱被测链路）：
//   - userRepo（corerepo.UserRepository）用内存 fake：生产 Gorm 实现 Preload 的 model.User 含
//     MySQL enum 列，与 sqlite 不兼容（迭代 001 已同法回避）；vuln service 只调 FindByID。
//     "主人 DB 权限"由此 fake 的角色-权限表提供，语义与生产 RBAC 解析结果一致。
//   - employeeRepo（directory.EmployeeDirectory）用内存 fake：被测路径仅 ValidateEmployeeIDs
//     调 FindByID。
//   - PublicID 序列：GormVulnerabilityRepository.Create 内的自增序列 SQL 是 MySQL 方言
//     （ON DUPLICATE KEY UPDATE / LAST_INSERT_ID()），sqlite 无法执行；测试用包装仓储在
//     PublicID 为空时先赋测试值再委托真实仓储，其余行为（事务、Preload、Updates）全走真实实现。
//   - storage.FileUploader 传 nil：两个 uploads 路由由此返回 501，恰好用于区分
//     "白名单已放行 vs 业务层未启用"。S3 预签名上传链路不在本 Phase 范围。
//
// 响应约定（与迭代 001 相同）：response.Error 恒返回 HTTP 200 + body code，
// 所有 401/403/501 断言按 body code，不断言 HTTP 状态码。
//
// 拒绝层级定位（本迭代用户要求：测试失败时能定位是哪一层拒绝）——四层 4xx 以
// body message 严格区分，每个断言都校验 message 所属层级：
//
//	层级 1 AgentAuth(scope)：     "agent scope not allowed"
//	层级 2 路由白名单：           "agent not allowed on this route"
//	层级 3 Permission 中间件：    "insufficient permissions"
//	层级 4 业务防护/求交：        "not authorized ..."（含具体字段或动作）
//	 uploads 业务未启用（非拒绝）：501 "file uploads are not enabled"
package e2e

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	agenthandler "security-center-backend/internal/handler/agent"
	"security-center-backend/internal/config"
	"security-center-backend/internal/middleware"
	"security-center-backend/internal/model"
	vulnhandler "security-center-backend/internal/modules/vuln/handler"
	vulnmodel "security-center-backend/internal/modules/vuln/model"
	vulnrepository "security-center-backend/internal/modules/vuln/repository"
	vulnservice "security-center-backend/internal/modules/vuln/service"
	"security-center-backend/internal/pkg/response"
	corerepo "security-center-backend/internal/repository"
	"security-center-backend/internal/service/audit"
	"security-center-backend/internal/service/directory"
	"security-center-backend/internal/service/jwt"
)

const (
	p1wOwnerUserID = uint64(42) // API Key 主人
	p1wHumanUserID = uint64(7)  // 人类安全工程师（回归用）

	p1wDiscovererEmployeeID = uint64(11)
	p1wFixerEmployeeID      = uint64(12)

	p1wWriteKeyRaw = "sk-agent-e2e-p1w-write-key" // 含 4 个写 scope + view
	p1wReadKeyRaw  = "sk-agent-e2e-p1w-read-key"  // 仅 view（求交验证用）
	p1wDenyKeyRaw  = "sk-agent-e2e-p1w-deny-key"  // denylist scope（请求时拒绝验证用）

	p1wWriteAgentName = "e2e-write-bot"
	p1wReadAgentName  = "e2e-read-bot"
	p1wDenyAgentName  = "e2e-deny-bot"
)

// 各层拒绝文案（见文件头注释的层级表），断言时逐层比对。
const (
	msgAgentScopeDenied  = "agent scope not allowed"              // 层级 1：AgentAuth
	msgRouteBlocked      = "agent not allowed on this route"      // 层级 2：路由白名单
	msgInsufficientPerms = "insufficient permissions"             // 层级 3：Permission 中间件
	msgUploadsDisabled   = "file uploads are not enabled"         // uploads 业务未启用（501，非 403）
	msgCreateKeyScopeDenied = "scope exceeds your permissions or not allowed for agent" // 建 Key scope 校验
)

// p1wUser 镜像 users 表被 Preload 选取的最小列集，避开 model.User 的 MySQL enum
// 与 sqlite 不兼容（迭代 001 phase1User 同款套路，加 avatar_url 列）。
type p1wUser struct {
	model.Base
	Name      string `gorm:"size:255"`
	Email     string `gorm:"size:320"`
	AvatarURL string `gorm:"size:512"`
	// AgentAuth 校验主人状态 (plan 003 §5.2①), 镜像表需带 status 列; 默认 active。
	Status string `gorm:"size:20;default:'active'"`
}

func (p1wUser) TableName() string { return "users" }

// p1wUserRepo 是 corerepo.UserRepository 的内存实现：仅 FindByID 有真实语义
// （vuln service resolveActor 唯一调用点），其余方法本测试路径不触及。
type p1wUserRepo struct {
	users map[uint64]*model.User
}

func (r *p1wUserRepo) FindByID(_ context.Context, id uint64) (*model.User, error) {
	if u, ok := r.users[id]; ok {
		return u, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (r *p1wUserRepo) FindByAuth0ID(context.Context, string) (*model.User, error) {
	return nil, gorm.ErrRecordNotFound
}
func (r *p1wUserRepo) FindByExternalID(context.Context, string) (*model.User, error) {
	return nil, gorm.ErrRecordNotFound
}
func (r *p1wUserRepo) FindByEmail(context.Context, string) (*model.User, error) {
	return nil, gorm.ErrRecordNotFound
}
func (r *p1wUserRepo) FindByEmails(context.Context, []string) ([]model.User, error) {
	return nil, nil
}
func (r *p1wUserRepo) CreateOrUpdate(context.Context, *model.User) error { return nil }
func (r *p1wUserRepo) List(context.Context, corerepo.UserFilter, int, int) ([]model.User, int64, error) {
	return nil, 0, nil
}
func (r *p1wUserRepo) AssignRoles(context.Context, uint64, []uint64) error    { return nil }
func (r *p1wUserRepo) RemoveRole(context.Context, uint64, uint64) error       { return nil }
func (r *p1wUserRepo) GetUserRoles(context.Context, uint64) ([]model.Role, error) {
	return nil, nil
}
func (r *p1wUserRepo) GetUserPermissions(context.Context, uint64) ([]model.Permission, error) {
	return nil, nil
}
func (r *p1wUserRepo) UpdateDefaultRole(context.Context, uint64, *uint64) error { return nil }

// p1wEmployeeDir 是 directory.EmployeeDirectory 的内存实现（被测路径仅调 FindByID）。
type p1wEmployeeDir struct {
	employees map[uint64]*directory.Employee
}

func (d *p1wEmployeeDir) FindByID(_ context.Context, id uint64) (*directory.Employee, error) {
	if e, ok := d.employees[id]; ok {
		return e, nil
	}
	return nil, gorm.ErrRecordNotFound
}
func (d *p1wEmployeeDir) FindByEmail(context.Context, string) (*directory.Employee, error) {
	return nil, gorm.ErrRecordNotFound
}
func (d *p1wEmployeeDir) FindByEmails(context.Context, []string) ([]directory.Employee, error) {
	return nil, nil
}
func (d *p1wEmployeeDir) SearchEmployees(context.Context, string) ([]directory.Employee, error) {
	return nil, nil
}
func (d *p1wEmployeeDir) ListEmployees(context.Context) ([]directory.Employee, error) {
	return nil, nil
}

// p1wVulnRepo 包装真实 GormVulnerabilityRepository：Create 内 PublicID 自增序列是
// MySQL 方言（ON DUPLICATE KEY UPDATE / LAST_INSERT_ID()），sqlite 无法执行，
// 这里在 PublicID 为空时先赋测试值再委托真实实现；其余方法全部透传真实仓储。
type p1wVulnRepo struct {
	vulnrepository.VulnerabilityRepository
	seq int64
}

func (r *p1wVulnRepo) Create(ctx context.Context, vuln *vulnmodel.Vulnerability) error {
	if strings.TrimSpace(vuln.PublicID) == "" {
		vuln.PublicID = fmt.Sprintf("vul-e2e-%04d", atomic.AddInt64(&r.seq, 1))
	}
	return r.VulnerabilityRepository.Create(ctx, vuln)
}

// p1wSuite 持有进程内集成测试的全部依赖。
type p1wSuite struct {
	db       *gorm.DB
	engine   *gin.Engine
	jwtMgr   *jwt.Manager
	vulnRepo vulnrepository.VulnerabilityRepository

	probeIngest *atomic.Bool // POST /caasm/ingest 桩是否被执行（应永远不被 agent 触达）
	probeDelete *atomic.Bool // DELETE /vulnerabilities/:public_id 桩是否被执行
}

func newP1wSuite(t *testing.T) *p1wSuite {
	t.Helper()
	gin.SetMode(gin.TestMode)

	// DSN 带纳秒后缀：同一进程内 -count=N 重复运行时各拿全新库，保证幂等。
	dsn := fmt.Sprintf("file:%s-%d?mode=memory&cache=shared", t.Name(), time.Now().UnixNano())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&p1wUser{},
		&directory.Employee{},
		&directory.Department{},
		&vulnmodel.Vulnerability{},
		&vulnmodel.VulnerabilityEvent{},
		&vulnmodel.VulnerabilityComment{},
		&model.APIKey{},
		&model.AuditLog{},
	))

	ctx := context.Background()
	require.NoError(t, db.Create(&directory.Employee{ID: p1wDiscovererEmployeeID, Name: "Discoverer", Email: "disc@example.com", LarkUserID: "lark-disc"}).Error)
	require.NoError(t, db.Create(&directory.Employee{ID: p1wFixerEmployeeID, Name: "Fixer", Email: "fix@example.com", LarkUserID: "lark-fix"}).Error)
	require.NoError(t, db.Create(&p1wUser{Base: model.Base{ID: p1wOwnerUserID}, Name: "Key Owner", Email: "owner@example.com"}).Error)
	require.NoError(t, db.Create(&p1wUser{Base: model.Base{ID: p1wHumanUserID}, Name: "Sec Human", Email: "sec@example.com"}).Error)

	// 三把 Key 经真实 Gorm APIKey 仓储 sha256 插库（与迭代 001 同套路）。
	keyRepo := corerepo.NewAPIKeyRepository(db)
	insertKey := func(raw, name string, scopes ...string) {
		key := &model.APIKey{
			KeyPrefix:       raw[:16],
			KeyHash:         sha256Hex(raw),
			AgentName:       name,
			OwnerUserID:     p1wOwnerUserID,
			PermissionScope: model.StringArray(scopes),
			Status:          model.APIKeyStatusActive,
		}
		require.NoError(t, keyRepo.Create(ctx, key))
	}
	insertKey(p1wWriteKeyRaw, p1wWriteAgentName,
		"vuln:view_findings", "vuln:edit_details", "vuln:request_retest", "vuln:close_findings",
		"vuln:submit_findings")
	insertKey(p1wReadKeyRaw, p1wReadAgentName, "vuln:view_findings")
	// denylist scope 的 Key 无法经建 Key 端点创建（②会验证），直接插库模拟存量/异常数据。
	insertKey(p1wDenyKeyRaw, p1wDenyAgentName, "vuln:manage_findings")

	// 内存 userRepo：模拟"主人 DB 权限"。42 号主人持全部 5 个 vuln 权限（求交左侧）；
	// 7 号人类持回归所需权限。角色清空后 admin 绕过失效的语义由 P1-T3 单测覆盖。
	employeeOf := func(id uint64) *uint64 { return &id }
	userRepo := &p1wUserRepo{users: map[uint64]*model.User{
		p1wOwnerUserID: {
			Base:       model.Base{ID: p1wOwnerUserID},
			Email:      "owner@example.com",
			EmployeeID: employeeOf(p1wFixerEmployeeID),
			Roles: []model.Role{{Name: "vuln-ops", Permissions: []model.Permission{
				{Name: "vuln:view_findings"}, {Name: "vuln:edit_details"},
				{Name: "vuln:request_retest"}, {Name: "vuln:close_findings"},
				{Name: "vuln:submit_findings"},
			}}},
		},
		p1wHumanUserID: {
			Base:       model.Base{ID: p1wHumanUserID},
			Email:      "sec@example.com",
			EmployeeID: employeeOf(p1wDiscovererEmployeeID),
			Roles: []model.Role{{Name: "sec-engineer", Permissions: []model.Permission{
				{Name: "vuln:view_findings"}, {Name: "vuln:edit_details"},
				{Name: "vuln:edit_sensitive_details"}, {Name: "vuln:request_retest"},
				{Name: "vuln:close_findings"}, {Name: "vuln:submit_findings"},
			}}},
		},
	}}
	employeeDir := &p1wEmployeeDir{employees: map[uint64]*directory.Employee{
		p1wDiscovererEmployeeID: {ID: p1wDiscovererEmployeeID, Name: "Discoverer", Email: "disc@example.com"},
		p1wFixerEmployeeID:      {ID: p1wFixerEmployeeID, Name: "Fixer", Email: "fix@example.com"},
	}}

	jwtMgr := jwt.NewManager(config.JWTConfig{
		AccessSecret:     "p1w-e2e-access-secret",
		RefreshSecret:    "p1w-e2e-refresh-secret",
		AccessTokenTTL:   time.Hour,
		RefreshTokenTTL:  24 * time.Hour,
		SigningAlgorithm: "HS256",
		Issuer:           "p1w-e2e",
	})

	auditSvc := audit.NewService(corerepo.NewAuditRepository(db), nil, nil, 0, nil)
	t.Cleanup(func() {
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = auditSvc.Shutdown(shutdownCtx)
	})

	// 真实 vuln service + handler：仓储真实（PublicID 序列包装除外），
	// channel/type/source/sla/requiredField/attach 六个仓储传 nil —— 被测路径
	// （Create/Update/Transition/AddComment/ListEvents/Get）对它们都有 nil 分支，
	// requiredField 为 nil 时走 DefaultSubmissionRequiredFields（与建单校验一致）。
	vulnRepo := &p1wVulnRepo{VulnerabilityRepository: vulnrepository.NewVulnerabilityRepository(db)}
	vulnSvc := vulnservice.New(
		vulnRepo,
		nil, nil, nil,
		vulnrepository.NewVulnerabilityEventRepository(db),
		vulnrepository.NewVulnerabilityCommentRepository(db),
		nil, nil, nil,
		userRepo,
		employeeDir,
	)
	vulnHandler := vulnhandler.New(vulnSvc, nil, nil) // uploader=nil → uploads 501；cache nil 安全

	agentCfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	agentKeysHandler := agenthandler.New(keyRepo, agentCfg)

	// 与 router.go 生产装配同序：
	// CombinedAuth → AgentRouteAllowlist → AuditTrail → Permission(路由级)。
	engine := gin.New()
	api := engine.Group("/api/v1")
	api.Use(middleware.CombinedAuth(
		middleware.AgentAuth(keyRepo, agentCfg),
		middleware.Auth(jwtMgr),
	))
	api.Use(middleware.AgentRouteAllowlist())
	api.Use(middleware.AuditTrail(auditSvc))

	// 建 Key 端点（生产：protected.Group("/agent/api-keys") + Permission("agent:manage_own_keys")）。
	agentGroup := api.Group("/agent/api-keys")
	agentGroup.Use(middleware.Permission("agent:manage_own_keys"))
	agentKeysHandler.RegisterRoutes(agentGroup)

	// 漏洞受限面（生产：Module.RegisterRoutes → RegisterRestrictedRoutes 挂
	// Permission("vuln:view_findings") 组，余 15 条受限路由在此组）。
	vulnGroup := api.Group("")
	vulnGroup.Use(middleware.Permission("vuln:view_findings"))
	vulnHandler.RegisterRestrictedRoutes(vulnGroup)

	// 提交面（iteration-003 P1-T2：Create 拆出 view 组，挂平级
	// Permission("vuln:submit_findings") 子组，镜像 module.go 生产装配）。
	submitGroup := api.Group("")
	submitGroup.Use(middleware.Permission("vuln:submit_findings"))
	vulnHandler.RegisterSubmitRoutes(submitGroup)

	// 公开可见面与候选人面（生产同样挂在 protected 下、无路由级 Permission）：
	// 提供 GET /vulnerabilities、GET /vulnerabilities/:public_id 等白名单 GET。
	vulnHandler.RegisterPublicVisibilityRoutes(api.Group(""))
	vulnHandler.RegisterAuthorizedViewerCandidateRoutes(api.Group(""))

	// 越界探测桩：POST /caasm/ingest 复刻生产"已注册但不在白名单"的路由形态；
	// DELETE 桩用于验证白名单的 method 维度（生产未注册 DELETE，这里注册桩以证明
	// 拦在白名单层而非 404）。
	s := &p1wSuite{
		db:          db,
		engine:      engine,
		jwtMgr:      jwtMgr,
		vulnRepo:    vulnRepo,
		probeIngest: &atomic.Bool{},
		probeDelete: &atomic.Bool{},
	}
	api.POST("/caasm/ingest", func(c *gin.Context) {
		s.probeIngest.Store(true)
		response.Success(c, gin.H{"probe": "ingest"})
	})
	api.DELETE("/vulnerabilities/:public_id", func(c *gin.Context) {
		s.probeDelete.Store(true)
		response.Success(c, gin.H{"probe": "delete"})
	})
	return s
}

type p1wResult struct {
	httpStatus int
	body       response.APIResponse
}

// request 发进程内请求并解析统一信封。response.Error 恒 HTTP 200，断言按 body code。
func (s *p1wSuite) request(t *testing.T, method, path string, payload interface{}, headers map[string]string) p1wResult {
	t.Helper()
	var reader *bytes.Reader
	if payload != nil {
		raw, err := json.Marshal(payload)
		require.NoError(t, err)
		reader = bytes.NewReader(raw)
	} else {
		reader = bytes.NewReader(nil)
	}
	req := httptest.NewRequest(method, path, reader)
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	w := httptest.NewRecorder()
	s.engine.ServeHTTP(w, req)
	var body response.APIResponse
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body), "响应非统一信封 JSON, raw=%q", w.Body.String())
	return p1wResult{httpStatus: w.Code, body: body}
}

func (s *p1wSuite) agentHeaders(rawKey string) map[string]string {
	return map[string]string{"X-API-Key": rawKey}
}

// humanToken 签发 7 号人类的 JWT，权限按用例需要显式给出（不借 admin 角色，更贴近真实）。
func (s *p1wSuite) humanToken(t *testing.T, permissions ...string) string {
	t.Helper()
	token, err := s.jwtMgr.GenerateAccessToken(p1wHumanUserID, "sec@example.com", nil, permissions)
	require.NoError(t, err)
	return token
}

func (s *p1wSuite) humanHeaders(t *testing.T, permissions ...string) map[string]string {
	t.Helper()
	return map[string]string{"Authorization": "Bearer " + s.humanToken(t, permissions...)}
}

// dataMap 取成功响应的 data 对象（map 形态）。
func (r p1wResult) dataMap(t *testing.T) map[string]interface{} {
	t.Helper()
	data, ok := r.body.Data.(map[string]interface{})
	require.True(t, ok, "成功响应应携带 data 对象, body=%+v", r.body)
	return data
}

// dataList 取成功响应的 data 数组（events/comments 列表形态）。
func (r p1wResult) dataList(t *testing.T) []interface{} {
	t.Helper()
	list, ok := r.body.Data.([]interface{})
	require.True(t, ok, "成功响应应携带 data 数组, body=%+v", r.body)
	return list
}

// findEvent 在事件列表里按 event_type 找第一条，事件项为 map（含 metadata）。
func p1wFindEvent(t *testing.T, events []interface{}, eventType string) map[string]interface{} {
	t.Helper()
	for _, item := range events {
		event, ok := item.(map[string]interface{})
		require.True(t, ok, "事件项应为对象, item=%+v", item)
		if event["event_type"] == eventType {
			return event
		}
	}
	t.Fatalf("事件流中未找到 event_type=%q, events=%v", eventType, events)
	return nil
}

func p1wEventMeta(t *testing.T, event map[string]interface{}) map[string]interface{} {
	t.Helper()
	meta, ok := event["metadata"].(map[string]interface{})
	require.True(t, ok, "事件应携带 metadata 对象, event=%+v", event)
	return meta
}

// seedVuln 直接经真实仓储（PublicID 已包装）造一条漏洞，返回落库后的记录。
// 各子用例独立造数，互不依赖执行顺序，保证幂等可重复。
func (s *p1wSuite) seedVuln(t *testing.T, status vulnmodel.VulnerabilityStatus) *vulnmodel.Vulnerability {
	t.Helper()
	now := time.Now().UTC().Truncate(time.Second)
	vuln := &vulnmodel.Vulnerability{
		Title:                "seed title",
		SeverityLevel:        vulnmodel.SeverityHigh,
		Status:               status,
		DiscovererEmployeeID: ptrUint64(p1wDiscovererEmployeeID),
		FixerEmployeeID:      ptrUint64(p1wFixerEmployeeID),
		SubmitterUserID:      p1wOwnerUserID,
		VerificationSteps:    "seed steps",
		POCText:              "seed poc",
		SubmittedAt:          now,
		SLADeadline:          now.Add(72 * time.Hour),
	}
	require.NoError(t, s.vulnRepo.Create(context.Background(), vuln))
	created, err := s.vulnRepo.FindByPublicID(context.Background(), vuln.PublicID)
	require.NoError(t, err)
	return created
}

func ptrUint64(v uint64) *uint64 { return &v }

// p1wCreatePayload 返回满足 DefaultSubmissionRequiredFields 的合法建单请求体。
func p1wCreatePayload() map[string]interface{} {
	return map[string]interface{}{
		"title":                  "e2e created vuln",
		"severity_level":         "high",
		"status":                 "active",
		"discoverer_employee_id": p1wDiscovererEmployeeID,
		"fixer_employee_id":      p1wFixerEmployeeID,
		"vulnerability_type":     "xss",
		"application":            "shop-web",
		"source":                 "scanner",
		"discovery_phase":        "prod",
		"network_env":            "internet",
		"description":            "<p>found by e2e</p>",
	}
}

func (s *p1wSuite) requireVuln(t *testing.T, publicID string) *vulnmodel.Vulnerability {
	t.Helper()
	vuln, err := s.vulnRepo.FindByPublicID(context.Background(), publicID)
	require.NoError(t, err)
	return vuln
}

func TestPhase1VulnWriteIntegration(t *testing.T) {
	s := newP1wSuite(t)
	ctx := context.Background()

	// ① 白名单：6 条写路由按 method 放行；越界 403。
	t.Run("1_route_allowlist", func(t *testing.T) {
		t.Run("1a_create_route_allowed", func(t *testing.T) {
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", p1wCreatePayload(), s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"白名单应放行 POST /vulnerabilities 且业务成功; 若 403 看 message 定位层级: %s", res.body.Message)
			require.NotEmpty(t, res.dataMap(t)["public_id"], "建单应返回 public_id")
		})

		t.Run("1b_update_route_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{"title": "renamed by agent"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"白名单应放行 PUT /vulnerabilities/:public_id 且业务成功; message=%s", res.body.Message)
			require.Equal(t, "renamed by agent", res.dataMap(t)["title"])
		})

		t.Run("1c_transition_route_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusRetesting)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/transition",
				map[string]interface{}{"status": "fixed", "message": "verified"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"白名单应放行 POST transition 且业务成功; message=%s", res.body.Message)
			require.Equal(t, "fixed", res.dataMap(t)["status"])
		})

		t.Run("1d_comment_route_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/comments",
				map[string]interface{}{"content": "agent comment"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"白名单应放行 POST comments 且业务成功; message=%s", res.body.Message)
			require.Equal(t, "agent comment", res.dataMap(t)["content"])
		})

		t.Run("1e_upload_route_allowed_business_501", func(t *testing.T) {
			// uploader=nil → 业务层 501。501（而非白名单 403）证明请求穿过了白名单与
			// Permission 两层到达 handler —— "放行"与"业务未启用"是两个层级。
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/uploads",
				map[string]interface{}{"filename": "poc.png"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, http.StatusNotImplemented, res.body.Code,
				"uploads 应穿过白名单到达业务层返回 501; 若 403 则是白名单误拦, message=%s", res.body.Message)
			require.Equal(t, msgUploadsDisabled, res.body.Message)
		})

		t.Run("1f_upload_confirm_route_allowed_business_501", func(t *testing.T) {
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/uploads/confirm",
				map[string]interface{}{"attachment_id": 1}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, http.StatusNotImplemented, res.body.Code,
				"uploads/confirm 应穿过白名单到达业务层返回 501; message=%s", res.body.Message)
			require.Equal(t, msgUploadsDisabled, res.body.Message)
		})

		t.Run("1g_delete_out_of_allowlist_403", func(t *testing.T) {
			s.probeDelete.Store(false)
			res := s.request(t, http.MethodDelete, "/api/v1/vulnerabilities/vul-e2e-whatever", nil, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgRouteBlocked, res.body.Message,
				"DELETE 应被层级 2（路由白名单）拦截")
			require.False(t, s.probeDelete.Load(), "被拦截时桩 handler 不应执行")
		})

		t.Run("1h_post_outside_allowlist_403", func(t *testing.T) {
			s.probeIngest.Store(false)
			res := s.request(t, http.MethodPost, "/api/v1/caasm/ingest",
				map[string]interface{}{"x": 1}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgRouteBlocked, res.body.Message,
				"白名单外 POST 应被层级 2（路由白名单）拦截")
			require.False(t, s.probeIngest.Load(), "被拦截时桩 handler 不应执行")
		})

		t.Run("1i_claim_route_not_opened_403", func(t *testing.T) {
			// Claim 路由在生产已注册（RegisterRestrictedRoutes）但 plan §5 明确不对 agent 开放。
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/claim",
				map[string]interface{}{}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgRouteBlocked, res.body.Message,
				"Claim 未在白名单内，应被层级 2 拦截")
		})

		t.Run("1j_get_routes_still_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodGet, "/api/v1/vulnerabilities/"+vuln.PublicID, nil, s.agentHeaders(p1wReadKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"GET 白名单面不应受写面开放影响; message=%s", res.body.Message)
		})
	})

	// ② scope：写 scope 建 Key 成功；denylist scope 建 Key 被拒；denylist Key 请求时被拒。
	t.Run("2_scope_enforcement", func(t *testing.T) {
		humanKeyMgrHeaders := func(t *testing.T, scopes ...string) map[string]string {
			perms := append([]string{"agent:manage_own_keys"}, scopes...)
			return s.humanHeaders(t, perms...)
		}

		t.Run("2a_create_key_with_write_scopes_succeeds", func(t *testing.T) {
			res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys",
				map[string]interface{}{
					"agent_name": "e2e-created-bot",
					"permission_scope": []string{
						"vuln:view_findings", "vuln:edit_details",
						"vuln:request_retest", "vuln:close_findings",
					},
				}, humanKeyMgrHeaders(t,
					"vuln:view_findings", "vuln:edit_details", "vuln:request_retest", "vuln:close_findings"))
			require.Equal(t, 0, res.body.Code,
				"写 scope 在白名单内且主人具备, 建 Key 应成功; message=%s", res.body.Message)
			plaintext, _ := res.dataMap(t)["api_key"].(string)
			require.True(t, strings.HasPrefix(plaintext, "sk-agent-"), "应返回一次性明文 Key, data=%+v", res.body.Data)

			// 新 Key 端到端可用：过 AgentAuth 的 scope 校验并调通白名单 GET。
			useRes := s.request(t, http.MethodGet, "/api/v1/vulnerabilities", nil, s.agentHeaders(plaintext))
			require.Equal(t, 0, useRes.body.Code,
				"新建的写 scope Key 应能调通接口; message=%s", useRes.body.Message)
		})

		t.Run("2b_create_key_with_denylist_scope_rejected", func(t *testing.T) {
			// 主人 JWT 具备 vuln:manage_findings（排除"超主人权限"因素），仍须被 denylist 拒。
			res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys",
				map[string]interface{}{
					"agent_name":       "e2e-deny-attempt",
					"permission_scope": []string{"vuln:manage_findings"},
				}, humanKeyMgrHeaders(t, "vuln:manage_findings"))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgCreateKeyScopeDenied, res.body.Message,
				"denylist scope 建 Key 应被拒（denylist 优先于主人权限）")
		})

		t.Run("2c_create_key_scope_exceeds_owner_rejected", func(t *testing.T) {
			// vuln:edit_details 不在主人 JWT 权限内 → 超主人权限，同样 403。
			res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys",
				map[string]interface{}{
					"agent_name":       "e2e-exceed-attempt",
					"permission_scope": []string{"vuln:edit_details"},
				}, humanKeyMgrHeaders(t, "vuln:view_findings"))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgCreateKeyScopeDenied, res.body.Message)
		})

		t.Run("2d_request_with_denylist_scope_key_rejected", func(t *testing.T) {
			// 存量 denylist scope Key（直接插库）请求时被层级 1（AgentAuth scope 校验）拒。
			res := s.request(t, http.MethodGet, "/api/v1/vulnerabilities", nil, s.agentHeaders(p1wDenyKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgAgentScopeDenied, res.body.Message,
				"denylist scope 应在层级 1（AgentAuth）被拒，区别于白名单文案 %q", msgRouteBlocked)
		})
	})

	// ③ 权限求交：主人有 close_findings/edit_details 但 Key 没有 → 业务层 403。
	t.Run("3_permission_intersection", func(t *testing.T) {
		t.Run("3a_key_lacks_close_findings_mark_fixed_403", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusRetesting)
			// 只读 Key：穿过白名单（POST transition 在列）、穿过 Permission（view_findings 满足），
			// 在层级 4 被"主人权限 ∩ Key scope"拒（主人 DB 有 close_findings，Key 没有）。
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/transition",
				map[string]interface{}{"status": "fixed"}, s.agentHeaders(p1wReadKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Contains(t, res.body.Message, "not authorized",
				"应为层级 4（业务求交）拒绝, 而非白名单 %q 或 Permission %q", msgRouteBlocked, msgInsufficientPerms)

			stored := s.requireVuln(t, vuln.PublicID)
			require.Equal(t, vulnmodel.StatusRetesting, stored.Status, "被拒后状态不得落库变更")
		})

		t.Run("3b_key_with_close_findings_mark_fixed_ok", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusRetesting)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/transition",
				map[string]interface{}{"status": "fixed", "message": "verified by agent"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"Key 与主人都有 close_findings 时应放行（求交为正例）; message=%s", res.body.Message)
			require.Equal(t, "fixed", res.dataMap(t)["status"])
		})

		t.Run("3c_key_lacks_edit_details_update_403", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{"title": "readonly key must fail"}, s.agentHeaders(p1wReadKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Contains(t, res.body.Message, "not authorized",
				"主人有 edit_details 但只读 Key 没有, 应为层级 4 拒绝")

			stored := s.requireVuln(t, vuln.PublicID)
			require.Equal(t, "seed title", stored.Title, "被拒后字段不得落库变更")
		})
	})

	// ④ 三条写防护：敏感字段 403、时间字段 403、Create submitter 强制 = Key 主人。
	t.Run("4_write_guards", func(t *testing.T) {
		t.Run("4a_sensitive_fields_change_403", func(t *testing.T) {
			cases := []struct {
				name    string
				field   string
				payload map[string]interface{}
				assert  func(t *testing.T, stored *vulnmodel.Vulnerability)
			}{
				{"poc_text", "poc_text",
					map[string]interface{}{"poc_text": "evil poc"},
					func(t *testing.T, v *vulnmodel.Vulnerability) { require.Equal(t, "seed poc", v.POCText) }},
				{"verification_steps", "verification_steps",
					map[string]interface{}{"verification_steps": "evil steps"},
					func(t *testing.T, v *vulnmodel.Vulnerability) { require.Equal(t, "seed steps", v.VerificationSteps) }},
				{"has_poc_verification", "has_poc_verification",
					map[string]interface{}{"has_poc_verification": true},
					func(t *testing.T, v *vulnmodel.Vulnerability) { require.False(t, v.HasPOCVerification) }},
			}
			for _, tc := range cases {
				t.Run(tc.name, func(t *testing.T) {
					vuln := s.seedVuln(t, vulnmodel.StatusActive)
					res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
						tc.payload, s.agentHeaders(p1wWriteKeyRaw))
					require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
					require.Contains(t, res.body.Message, "not authorized",
						"决策 6 防盲改应为层级 4（业务防护）拒绝")
					require.Contains(t, res.body.Message, tc.field, "错误应指明被拒字段")

					stored := s.requireVuln(t, vuln.PublicID)
					tc.assert(t, stored)
				})
			}
		})

		t.Run("4b_time_fields_change_403", func(t *testing.T) {
			later := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339)
			cases := []struct {
				name    string
				field   string
				payload map[string]interface{}
			}{
				{"submitted_at", "submitted_at", map[string]interface{}{"submitted_at": later}},
				{"sla_deadline", "sla_deadline", map[string]interface{}{"sla_deadline": later}},
			}
			for _, tc := range cases {
				t.Run(tc.name, func(t *testing.T) {
					vuln := s.seedVuln(t, vulnmodel.StatusActive)
					res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
						tc.payload, s.agentHeaders(p1wWriteKeyRaw))
					require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
					require.Contains(t, res.body.Message, "not authorized",
						"决策 7 防 SLA 造假应为层级 4（业务防护）拒绝")
					require.Contains(t, res.body.Message, tc.field, "错误应指明被拒字段")
				})
			}
		})

		t.Run("4c_same_value_and_normal_field_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{
					"poc_text":           "seed poc",   // 同值不算变更，不触发防护
					"verification_steps": "seed steps", // 同值
					"title":              "agent renamed in guard test",
				}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code,
				"同值 + 普通字段应放行; message=%s", res.body.Message)
			stored := s.requireVuln(t, vuln.PublicID)
			require.Equal(t, "agent renamed in guard test", stored.Title)
		})

		t.Run("4d_create_forces_owner_submitter_and_server_times", func(t *testing.T) {
			past := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
			payload := p1wCreatePayload()
			payload["submitter_user_id"] = 999 // 试图冒充他人
			payload["submitted_at"] = past.Format(time.RFC3339)
			payload["sla_deadline"] = past.Add(time.Hour).Format(time.RFC3339)

			before := time.Now().UTC()
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", payload, s.agentHeaders(p1wWriteKeyRaw))
			after := time.Now().UTC()
			require.Equal(t, 0, res.body.Code,
				"agent 建单应成功（防护是强制覆盖而非拒绝）; message=%s", res.body.Message)
			data := res.dataMap(t)
			require.Equal(t, float64(p1wOwnerUserID), data["submitter_user_id"],
				"决策 8：submitter 必须被强制为 Key 主人, data=%+v", data)

			submittedAt, err := time.Parse(time.RFC3339Nano, data["submitted_at"].(string))
			require.NoError(t, err)
			slaDeadline, err := time.Parse(time.RFC3339Nano, data["sla_deadline"].(string))
			require.NoError(t, err)
			require.False(t, submittedAt.Before(before), "决策 7：submitted_at 应为服务端当前时间，忽略请求体的 2020 值")
			require.False(t, submittedAt.After(after))
			require.Equal(t, submittedAt.Add(72*time.Hour), slaDeadline,
				"high 级别 sla_deadline 应为 submitted_at + 72h 服务端默认")

			publicID, _ := data["public_id"].(string)
			require.NotEmpty(t, publicID)
			stored := s.requireVuln(t, publicID)
			require.Equal(t, p1wOwnerUserID, stored.SubmitterUserID, "落库的 submitter 也必须是 Key 主人")
		})
	})

	// ⑤ 归因：agent 评论/提交后作者与事件 actor_name 为 Key 名（非空）。
	t.Run("5_attribution", func(t *testing.T) {
		t.Run("5a_comment_author_is_agent_key_name", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/comments",
				map[string]interface{}{"content": "from agent"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code, "message=%s", res.body.Message)
			require.Equal(t, p1wWriteAgentName, res.dataMap(t)["author_name"],
				"评论作者应为 Key 名（修缺口二，非空非 System）")

			listRes := s.request(t, http.MethodGet, "/api/v1/vulnerabilities/"+vuln.PublicID+"/comments",
				nil, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, listRes.body.Code)
			comments := listRes.dataList(t)
			require.Len(t, comments, 1)
			comment, ok := comments[0].(map[string]interface{})
			require.True(t, ok)
			require.Equal(t, p1wWriteAgentName, comment["author_name"], "评论列表中的作者同样应为 Key 名")
			require.Equal(t, float64(p1wOwnerUserID), comment["author_id"], "author_id 仍为 Key 主人")
		})

		t.Run("5b_event_actor_name_is_agent_key_name", func(t *testing.T) {
			// 建单事件
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", p1wCreatePayload(), s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code, "message=%s", res.body.Message)
			publicID, _ := res.dataMap(t)["public_id"].(string)
			require.NotEmpty(t, publicID)

			// 评论事件
			_, err := s.vulnRepo.FindByPublicID(ctx, publicID) // 确保已落库
			require.NoError(t, err)
			commentRes := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+publicID+"/comments",
				map[string]interface{}{"content": "event attribution"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, commentRes.body.Code, "message=%s", commentRes.body.Message)

			eventsRes := s.request(t, http.MethodGet, "/api/v1/vulnerabilities/"+publicID+"/events",
				nil, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, eventsRes.body.Code)
			events := eventsRes.dataList(t)

			submitted := p1wFindEvent(t, events, "submitted")
			submittedMeta := p1wEventMeta(t, submitted)
			require.Equal(t, p1wWriteAgentName, submittedMeta["actor_name"],
				"submitted 事件 actor_name 应为 Key 名（非空）, meta=%+v", submittedMeta)

			commentAdded := p1wFindEvent(t, events, "comment_added")
			commentMeta := p1wEventMeta(t, commentAdded)
			require.Equal(t, p1wWriteAgentName, commentMeta["actor_name"],
				"comment_added 事件 actor_name 应为 Key 名（非空）, meta=%+v", commentMeta)
		})
	})

	// ⑥ 旧值：编辑普通字段后事件 meta 含 previous_values；敏感字段旧值不外泄。
	t.Run("6_previous_values", func(t *testing.T) {
		t.Run("6a_normal_field_edit_records_previous_values", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{"title": "after-title"}, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, res.body.Code, "message=%s", res.body.Message)

			eventsRes := s.request(t, http.MethodGet, "/api/v1/vulnerabilities/"+vuln.PublicID+"/events",
				nil, s.agentHeaders(p1wWriteKeyRaw))
			require.Equal(t, 0, eventsRes.body.Code)
			event := p1wFindEvent(t, eventsRes.dataList(t), "details_updated")
			meta := p1wEventMeta(t, event)

			previous, ok := meta["previous_values"].(map[string]interface{})
			require.True(t, ok, "details_updated 事件 meta 应含 previous_values, meta=%+v", meta)
			require.Equal(t, "seed title", previous["title"], "旧值应为改动前的 title")
			require.Contains(t, meta["changed_fields"], "title")
			require.Equal(t, p1wWriteAgentName, meta["actor_name"])
		})

		t.Run("6b_sensitive_field_old_value_not_leaked", func(t *testing.T) {
			// 敏感字段只有人类路径能改（agent 被决策 6 拦），事件不得记录其旧值（plan §6.2 防泄密）。
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{"poc_text": "human-new-poc", "title": "human-title"},
				s.humanHeaders(t, "vuln:view_findings", "vuln:edit_details", "vuln:edit_sensitive_details"))
			require.Equal(t, 0, res.body.Code, "人类改敏感字段应放行; message=%s", res.body.Message)

			eventsRes := s.request(t, http.MethodGet, "/api/v1/vulnerabilities/"+vuln.PublicID+"/events",
				nil, s.humanHeaders(t, "vuln:view_findings", "vuln:edit_details"))
			require.Equal(t, 0, eventsRes.body.Code)
			event := p1wFindEvent(t, eventsRes.dataList(t), "details_updated")
			meta := p1wEventMeta(t, event)

			previous, ok := meta["previous_values"].(map[string]interface{})
			require.True(t, ok, "人类编辑同样应记录 previous_values, meta=%+v", meta)
			require.Equal(t, "seed title", previous["title"])
			require.NotContains(t, previous, "poc_text",
				"敏感字段旧值不得写入事件（防泄密）, previous_values=%+v", previous)
		})
	})

	// ⑦ 回归：人类 JWT 走原有写接口行为不变。
	t.Run("7_human_jwt_regression", func(t *testing.T) {
		t.Run("7a_human_create_keeps_request_body_values", func(t *testing.T) {
			past := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
			customSLA := past.Add(30 * 24 * time.Hour)
			payload := p1wCreatePayload()
			payload["submitter_user_id"] = 999 // 人类补录可指定任意提交人
			payload["submitted_at"] = past.Format(time.RFC3339)
			payload["sla_deadline"] = customSLA.Format(time.RFC3339)

			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", payload,
				s.humanHeaders(t, "vuln:view_findings", "vuln:edit_details", "vuln:submit_findings"))
			require.Equal(t, 0, res.body.Code, "人类建单应成功; message=%s", res.body.Message)
			data := res.dataMap(t)
			require.Equal(t, float64(999), data["submitter_user_id"], "人类建单 submitter 不被强制覆盖")

			submittedAt, err := time.Parse(time.RFC3339Nano, data["submitted_at"].(string))
			require.NoError(t, err)
			require.True(t, past.Equal(submittedAt), "人类可指定 submitted_at（补录）, got=%s", submittedAt)
			slaDeadline, err := time.Parse(time.RFC3339Nano, data["sla_deadline"].(string))
			require.NoError(t, err)
			require.True(t, customSLA.Equal(slaDeadline), "人类可指定 sla_deadline, got=%s", slaDeadline)
		})

		t.Run("7b_human_update_time_fields_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusActive)
			newSubmittedAt := time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)
			newSLA := newSubmittedAt.Add(48 * time.Hour)
			res := s.request(t, http.MethodPut, "/api/v1/vulnerabilities/"+vuln.PublicID,
				map[string]interface{}{
					"submitted_at": newSubmittedAt.Format(time.RFC3339),
					"sla_deadline": newSLA.Format(time.RFC3339),
				}, s.humanHeaders(t, "vuln:view_findings", "vuln:edit_details"))
			require.Equal(t, 0, res.body.Code,
				"人类改时间字段不受决策 7 防护限制; message=%s", res.body.Message)
			stored := s.requireVuln(t, vuln.PublicID)
			require.True(t, newSubmittedAt.Equal(stored.SubmittedAt.UTC()), "submitted_at 应落库, got=%s", stored.SubmittedAt)
			require.True(t, newSLA.Equal(stored.SLADeadline.UTC()), "sla_deadline 应落库, got=%s", stored.SLADeadline)
		})

		t.Run("7c_human_transition_allowed", func(t *testing.T) {
			vuln := s.seedVuln(t, vulnmodel.StatusRetesting)
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities/"+vuln.PublicID+"/transition",
				map[string]interface{}{"status": "fixed", "message": "human verified"},
				s.humanHeaders(t, "vuln:view_findings", "vuln:close_findings"))
			require.Equal(t, 0, res.body.Code, "人类状态流转行为不变; message=%s", res.body.Message)
			require.Equal(t, "fixed", res.dataMap(t)["status"])
		})

		t.Run("7d_human_not_blocked_by_agent_allowlist", func(t *testing.T) {
			s.probeDelete.Store(false)
			s.probeIngest.Store(false)
			human := s.humanHeaders(t)

			deleteRes := s.request(t, http.MethodDelete, "/api/v1/vulnerabilities/vul-e2e-whatever", nil, human)
			require.Equal(t, 0, deleteRes.body.Code, "人类 JWT 应直通白名单到达路由; body=%+v", deleteRes.body)
			require.True(t, s.probeDelete.Load(), "人类 DELETE 应到达桩 handler（白名单只约束 agent）")

			ingestRes := s.request(t, http.MethodPost, "/api/v1/caasm/ingest", map[string]interface{}{"x": 1}, human)
			require.Equal(t, 0, ingestRes.body.Code, "人类调白名单外路由不受限; body=%+v", ingestRes.body)
			require.True(t, s.probeIngest.Load())
		})
	})

	// ⑧ 提交门槛反向用例（iteration-003 P1-T2/P1-T5）：view-only 身份提交漏洞，
	// 穿过层级 1/2 后在层级 3（Permission 中间件）被 403。
	t.Run("8_submit_findings_gate", func(t *testing.T) {
		t.Run("8a_view_only_key_create_403", func(t *testing.T) {
			// 只读 Key（仅 vuln:view_findings scope）：POST /vulnerabilities 在白名单内
			// （层级 2 放行），但缺 submit_findings，应在层级 3 被拒。
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", p1wCreatePayload(), s.agentHeaders(p1wReadKeyRaw))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgInsufficientPerms, res.body.Message,
				"应为层级 3（Permission 中间件）拒绝, 而非白名单 %q 或业务层 not authorized", msgRouteBlocked)
		})

		t.Run("8b_view_only_human_create_403", func(t *testing.T) {
			// 人类 JWT 仅持 view_findings（无 submit_findings）：同样在层级 3 被拒。
			res := s.request(t, http.MethodPost, "/api/v1/vulnerabilities", p1wCreatePayload(),
				s.humanHeaders(t, "vuln:view_findings"))
			require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
			require.Equal(t, msgInsufficientPerms, res.body.Message,
				"人类缺 submit_findings 也应为层级 3 拒绝")
		})
	})
}
