// Phase 2（API Key 自助管理端点）的进程内集成验收（任务 P2-T9），
// 覆盖 docs/plan/001-agent-mcp-development-plan.md §2.5 的 6 条验收 + 2 条补充：
//
//	① 用户 A 创建 key（scope=["caasm:view_assets"]，带 expires_at）→ code 0，明文返回，owner 落库为 A
//	② A 列表只见自己的 key；A 用 B 的 key id Get → body code 404（先以 B 身份创建一把）
//	③ A 创建时 scope 含 "auth:manage_users"（自己没有的权限）→ body code 403
//	④ A 创建时 scope 含 "caasm:manage_assets"（只读白名单外）→ body code 403
//	⑤ A 吊销自己的 key → code 0 + status=revoked；补充 rotate：A rotate → 新明文返回、旧 id 已 revoked
//	    （吊销后该 key 走 AgentAuth → 401 的联动已由 Phase 1 phase1_integration_test.go 场景④验证，此处不重复）
//	⑥ admin 列表 → 能看到 A 和 B 的 key
//	补充：无 agent:manage_own_keys 权限的用户调创建 → Permission 层 body code 403
//
// 引导方式照抄 phase1_integration_test.go（P1-T14）：sqlite in-memory（shared cache）+
// 镜像 users 表 struct（复用本包 phase1User）+ 真实 GormAPIKeyRepository +
// 真实 agent Handler（agent.New(repo, cfg)）+ 真实 Permission("agent:manage_own_keys")。
// 与生产 router.go:132-138 的差异仅在认证层：生产为 CombinedAuth(OAuth 登录签发 JWT)，
// 进程内无法走 OAuth，按任务要求以身份 stub 注入 current_user claims（用户 A/B/admin/无权用户），
// stub 之后的 Permission 与 handler 链路全部为生产真实代码。
//
// 响应约定同 Phase 1：response.Error 恒 HTTP 200 + body code，断言一律按 body code。
package e2e

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

	"security-center-backend/internal/config"
	"security-center-backend/internal/handler/agent"
	"security-center-backend/internal/middleware"
	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/response"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/service/jwt"
)

const (
	phase2UserAID   = 101
	phase2UserBID   = 102
	phase2AdminID   = 1
	phase2NoPermUID = 103
)

// phase2Suite 持有进程内集成测试的全部依赖。
type phase2Suite struct {
	db     *gorm.DB
	engine *gin.Engine
	repo   repository.APIKeyRepository
}

// phase2IdentityStub 按 X-Test-Identity 头注入 current_user claims，
// 替代生产链路的 CombinedAuth（其后的 Permission + handler 均为真实代码）。
//   - "a"/"b": 普通用户，权限 ["agent:manage_own_keys","caasm:view_assets"]
//   - "admin": 角色 admin（Permission 对 admin 角色直通，List/Get/Revoke/Rotate 走 ListAll/FindByID 兜底）
//   - "noperm": 普通用户，只有 ["caasm:view_assets"]，无 agent:manage_own_keys
//   - 其他/缺省: 不注入 claims（Permission 层 401）
func phase2IdentityStub() gin.HandlerFunc {
	identities := map[string]*jwt.Claims{
		"a": {
			UserID:      phase2UserAID,
			Email:       "a@example.com",
			Roles:       []string{"user"},
			Permissions: []string{"agent:manage_own_keys", "caasm:view_assets"},
		},
		"b": {
			UserID:      phase2UserBID,
			Email:       "b@example.com",
			Roles:       []string{"user"},
			Permissions: []string{"agent:manage_own_keys", "caasm:view_assets"},
		},
		"admin": {
			UserID: phase2AdminID,
			Email:  "admin@example.com",
			Roles:  []string{"admin"},
		},
		"noperm": {
			UserID:      phase2NoPermUID,
			Email:       "noperm@example.com",
			Roles:       []string{"user"},
			Permissions: []string{"caasm:view_assets"},
		},
	}
	return func(c *gin.Context) {
		if claims, ok := identities[c.GetHeader("X-Test-Identity")]; ok {
			c.Set("current_user", claims)
		}
		c.Next()
	}
}

func newPhase2Suite(t *testing.T) *phase2Suite {
	t.Helper()
	gin.SetMode(gin.TestMode)

	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	// 镜像 users 表（复用本包 phase1User）必须先迁移：api_keys.owner_user_id 的 FK 指向它。
	require.NoError(t, db.AutoMigrate(&phase1User{}, &model.APIKey{}))
	for _, u := range []phase1User{
		{Base: model.Base{ID: phase2UserAID}, Name: "User A", Email: "a@example.com"},
		{Base: model.Base{ID: phase2UserBID}, Name: "User B", Email: "b@example.com"},
		{Base: model.Base{ID: phase2AdminID}, Name: "Admin", Email: "admin@example.com"},
		{Base: model.Base{ID: phase2NoPermUID}, Name: "No Perm", Email: "noperm@example.com"},
	} {
		require.NoError(t, db.Create(&u).Error)
	}

	repo := repository.NewAPIKeyRepository(db)
	// AllowPermanentKey=false 与生产一致（plan §2.6：expires_at 强制非永久），
	// 故所有创建请求都必须携带 expires_at。
	handler := agent.New(repo, config.AgentConfig{
		Enabled:           true,
		KeyPrefix:         "sk-agent-",
		AllowPermanentKey: false,
	})

	// 装配形态对齐 router.go:132-138：protected 组 → Permission → RegisterRoutes，
	// 仅认证层由 CombinedAuth 替换为身份 stub。
	engine := gin.New()
	api := engine.Group("/api/v1")
	api.Use(phase2IdentityStub())
	agentGroup := api.Group("/agent/api-keys")
	agentGroup.Use(middleware.Permission("agent:manage_own_keys"))
	handler.RegisterRoutes(agentGroup)

	return &phase2Suite{db: db, engine: engine, repo: repo}
}

// request 发进程内请求并解析统一信封（复用本包 phase1Result）。
func (s *phase2Suite) request(t *testing.T, method, path, identity string, payload interface{}) phase1Result {
	t.Helper()
	var reader io.Reader
	if payload != nil {
		raw, err := json.Marshal(payload)
		require.NoError(t, err)
		reader = bytes.NewReader(raw)
	}
	w := httptest.NewRecorder()
	req := httptest.NewRequest(method, path, reader)
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if identity != "" {
		req.Header.Set("X-Test-Identity", identity)
	}
	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 phase1Result{httpStatus: w.Code, body: body}
}

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

// createKey 以指定身份创建一把带 24h 过期时间的 key，断言成功并返回 (id, 明文)。
func (s *phase2Suite) createKey(t *testing.T, identity, name string, scope []string) (uint64, string) {
	t.Helper()
	expiresAt := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
	res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys", identity, map[string]interface{}{
		"agent_name":       name,
		"permission_scope": scope,
		"expires_at":       expiresAt,
	})
	require.Equal(t, 0, res.body.Code, "创建应成功, body=%+v", res.body)
	d := dataMap(t, res)
	id, ok := d["id"].(float64)
	require.True(t, ok && id > 0, "响应应携带 id, body=%+v", res.body)
	plaintext, _ := d["api_key"].(string)
	require.NotEmpty(t, plaintext, "创建响应应携带一次性明文")
	return uint64(id), plaintext
}

// listKeyIDs 以指定身份拉列表，断言成功并返回 id 集合。
func (s *phase2Suite) listKeyIDs(t *testing.T, identity string) map[uint64]bool {
	t.Helper()
	res := s.request(t, http.MethodGet, "/api/v1/agent/api-keys", identity, nil)
	require.Equal(t, 0, res.body.Code, "列表应成功, body=%+v", res.body)
	items, ok := res.body.Data.([]interface{})
	require.True(t, ok, "列表响应 data 应为数组, body=%+v", res.body)
	ids := make(map[uint64]bool, len(items))
	for _, item := range items {
		m, ok := item.(map[string]interface{})
		require.True(t, ok, "列表元素应为对象, body=%+v", res.body)
		id, ok := m["id"].(float64)
		require.True(t, ok, "列表元素应携带 id, body=%+v", res.body)
		ids[uint64(id)] = true
	}
	return ids
}

func TestPhase2KeyManagement(t *testing.T) {
	s := newPhase2Suite(t)

	t.Run("1_create_returns_plaintext_and_owner_is_A", func(t *testing.T) {
		expiresAt := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
		res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys", "a", map[string]interface{}{
			"agent_name":       "a-bot",
			"permission_scope": []string{"caasm:view_assets"},
			"expires_at":       expiresAt,
		})
		require.Equal(t, http.StatusOK, res.httpStatus, "信封约定: 成功分支 HTTP 200")
		require.Equal(t, 0, res.body.Code, "body=%+v", res.body)

		d := dataMap(t, res)
		plaintext, _ := d["api_key"].(string)
		require.NotEmpty(t, plaintext, "明文应随创建响应一次性返回")
		require.Contains(t, plaintext, "sk-agent-", "明文应带配置前缀")
		require.Equal(t, "active", d["status"])
		require.NotNil(t, d["expires_at"], "应回显 expires_at")
		_, leaksHash := d["key_hash"]
		require.False(t, leaksHash, "响应不得泄漏 key_hash")

		// owner 不取自请求体，由 handler 强制为当前用户 A，落库核验。
		id := uint64(d["id"].(float64))
		rec, err := s.repo.FindByID(context.Background(), id)
		require.NoError(t, err)
		require.NotNil(t, rec)
		require.Equal(t, uint64(phase2UserAID), rec.OwnerUserID, "key 的 owner 必须等于创建者 A")
		require.Equal(t, model.StringArray{"caasm:view_assets"}, rec.PermissionScope)
	})

	t.Run("2_list_isolation_and_cross_owner_get_404", func(t *testing.T) {
		bKeyID, _ := s.createKey(t, "b", "b-bot", []string{"caasm:view_assets"})
		aKeyID, _ := s.createKey(t, "a", "a-bot-isolation", []string{"caasm:view_assets"})

		ids := s.listKeyIDs(t, "a")
		require.True(t, ids[aKeyID], "A 的列表应包含 A 自己的 key")
		require.False(t, ids[bKeyID], "A 的列表不得包含 B 的 key")

		res := s.request(t, http.MethodGet, fmt.Sprintf("/api/v1/agent/api-keys/%d", bKeyID), "a", nil)
		require.Equal(t, http.StatusOK, res.httpStatus, "信封约定: 错误也返回 HTTP 200")
		require.Equal(t, http.StatusNotFound, res.body.Code, "越权 Get 应 404 且不得区分不存在/不属于自己, body=%+v", res.body)
		require.Nil(t, res.body.Data)
	})

	t.Run("3_create_scope_not_owned_403", func(t *testing.T) {
		res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys", "a", map[string]interface{}{
			"agent_name":       "a-bot-escalation",
			"permission_scope": []string{"caasm:view_assets", "auth:manage_users"},
			"expires_at":       time.Now().Add(24 * time.Hour).UTC(),
		})
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, http.StatusForbidden, res.body.Code, "scope 超出用户已有权限应 403, body=%+v", res.body)
		require.Contains(t, res.body.Message, "scope exceeds")
		require.Nil(t, res.body.Data, "被拒时不应产生 key")
	})

	t.Run("4_create_scope_outside_readonly_allowlist_403", func(t *testing.T) {
		// caasm:manage_assets 是写权限，不在 agentAllowedScopePrefixes 只读白名单内；
		// 同时 A 也不持有该权限——纵深防御两层任一都拒绝，结果同为 403。
		res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys", "a", map[string]interface{}{
			"agent_name":       "a-bot-write",
			"permission_scope": []string{"caasm:manage_assets"},
			"expires_at":       time.Now().Add(24 * time.Hour).UTC(),
		})
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, http.StatusForbidden, res.body.Code, "白名单外 scope 应 403, body=%+v", res.body)
		require.Contains(t, res.body.Message, "scope exceeds")
		require.Nil(t, res.body.Data)
	})

	t.Run("5_revoke_own_key_and_rotate", func(t *testing.T) {
		// 吊销：A 吊销自己的 key → code 0 + status=revoked。
		// 注：吊销后该 key 走 AgentAuth → 401 "api key revoked" 的联动
		// 已由 Phase 1（phase1_integration_test.go 场景④）验证，此处不重复。
		revokeID, _ := s.createKey(t, "a", "a-bot-revoke", []string{"caasm:view_assets"})
		res := s.request(t, http.MethodDelete, fmt.Sprintf("/api/v1/agent/api-keys/%d", revokeID), "a", nil)
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, 0, res.body.Code, "吊销自己的 key 应成功, body=%+v", res.body)
		require.Equal(t, "revoked", dataMap(t, res)["status"])

		// rotate：新明文一次性返回、id 换新、旧 id 已 revoked。
		oldID, oldPlaintext := s.createKey(t, "a", "a-bot-rotate", []string{"caasm:view_assets"})
		res = s.request(t, http.MethodPost, fmt.Sprintf("/api/v1/agent/api-keys/%d/rotate", oldID), "a", nil)
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, 0, res.body.Code, "rotate 应成功, body=%+v", res.body)
		d := dataMap(t, res)
		newPlaintext, _ := d["api_key"].(string)
		require.NotEmpty(t, newPlaintext, "rotate 应返回新明文")
		require.NotEqual(t, oldPlaintext, newPlaintext, "新明文不得与旧明文相同")
		newID := uint64(d["id"].(float64))
		require.NotEqual(t, oldID, newID, "rotate 应生成新 key id")
		require.Equal(t, "active", d["status"])

		res = s.request(t, http.MethodGet, fmt.Sprintf("/api/v1/agent/api-keys/%d", oldID), "a", nil)
		require.Equal(t, 0, res.body.Code, "body=%+v", res.body)
		require.Equal(t, "revoked", dataMap(t, res)["status"], "rotate 后旧 key 必须为 revoked")
	})

	t.Run("6_admin_lists_keys_of_all_owners", func(t *testing.T) {
		aKeyID, _ := s.createKey(t, "a", "a-bot-admin-view", []string{"caasm:view_assets"})
		bKeyID, _ := s.createKey(t, "b", "b-bot-admin-view", []string{"caasm:view_assets"})

		// admin 角色由 Permission 直通，List 走 ListAll 跨 owner 兜底。
		ids := s.listKeyIDs(t, "admin")
		require.True(t, ids[aKeyID], "admin 列表应包含 A 的 key")
		require.True(t, ids[bKeyID], "admin 列表应包含 B 的 key")
	})

	t.Run("7_user_without_manage_own_keys_403", func(t *testing.T) {
		res := s.request(t, http.MethodPost, "/api/v1/agent/api-keys", "noperm", map[string]interface{}{
			"agent_name":       "noperm-bot",
			"permission_scope": []string{"caasm:view_assets"},
			"expires_at":       time.Now().Add(24 * time.Hour).UTC(),
		})
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, http.StatusForbidden, res.body.Code, "无 agent:manage_own_keys 应被 Permission 拦截, body=%+v", res.body)
		require.Equal(t, "insufficient permissions", res.body.Message)
		require.Nil(t, res.body.Data, "handler 不应执行")

		keys, err := s.repo.ListByOwner(context.Background(), phase2NoPermUID)
		require.NoError(t, err)
		require.Empty(t, keys, "被 Permission 拦截的请求不得落库")
	})
}
