// Package e2e 存放跨进程/进程内的端到端验收测试。
//
// 本文件是 Phase 1（认证：API Key + Agent 中间件链）的集成验收（任务 P1-T14），
// 覆盖 docs/plan/001-agent-mcp-development-plan.md §1.9 的 5+1 条场景：
//
//	① 有效 agent key 调白名单内只读端点 → 下游执行（HTTP 200 + body code 0）
//	② 同 key 调 scope 不足的 /api/v1/users → body code 403
//	③ 同 key 调无权限码路由 POST /api/v1/caasm/ingest → body code 403（路由白名单拦截）
//	④ key 吊销后 → body code 401 "api key revoked"
//	⑤ 审计归因：audit_logs 产生 actor_type='agent' 且 metadata 含 agent_id 的记录
//	⑥ 回归：人类 JWT（admin 角色）调 /api/v1/users 正常放行，不受 agent 通道影响
//
// 响应约定（docs/tasks/001-agent-mcp-tasks-overview.md §5 第 5 条）：
// response.Error 恒返回 HTTP 200 + body code，故所有 401/403 断言均按 body code，
// 不断言 HTTP 状态码（成功分支同样恒为 HTTP 200，以 body code 0 + data 判定下游执行）。
//
// 引导方式照抄 internal/repository 既有测试：sqlite in-memory（shared cache）+
// 真实 GormAPIKeyRepository / GormAuditRepository + 真实中间件链
// CombinedAuth(AgentAuth, Auth) → AgentRouteAllowlist → AuditTrail → Permission。
// 不装配完整 router.RegisterRoutes（依赖过多 handler），只挂代表性路由。
package e2e

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"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/middleware"
	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/response"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/service/audit"
	"security-center-backend/internal/service/jwt"
)

const (
	phase1RawKey  = "sk-agent-e2e-phase1-test-key"
	phase1OwnerID = 42
	phase1AdminID = 1
)

// phase1User 镜像 users 表最小列集，避开 model.User 的 MySQL enum 与 sqlite 不兼容
// （与 internal/repository 既有测试的镜像 struct 套路一致）。
type phase1User struct {
	model.Base
	Name   string `gorm:"size:255"`
	Email  string `gorm:"size:320"`
	// AgentAuth 校验主人状态 (plan 003 §5.2①), 镜像表需带 status 列; 默认 active。
	Status string `gorm:"size:20;default:'active'"`
}

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

// phase1Suite 持有进程内集成测试的全部依赖。
type phase1Suite struct {
	db       *gorm.DB
	engine   *gin.Engine
	auditSvc *audit.Service
	keyID    uint64
	jwtMgr   *jwt.Manager
}

func sha256Hex(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

func newPhase1Suite(t *testing.T) *phase1Suite {
	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 表必须先迁移：api_keys.owner_user_id 与 audit_logs.actor_user_id 的 FK 指向它。
	require.NoError(t, db.AutoMigrate(&phase1User{}, &model.APIKey{}, &model.AuditLog{}))

	require.NoError(t, db.Create(&phase1User{Base: model.Base{ID: phase1OwnerID}, Name: "Key Owner", Email: "owner@example.com"}).Error)
	require.NoError(t, db.Create(&phase1User{Base: model.Base{ID: phase1AdminID}, Name: "Admin", Email: "admin@example.com"}).Error)

	// 真实 repository，sha256 插库造 key（plan §1.9 步骤 1：Phase 1 无端点，验收插库）。
	keyRepo := repository.NewAPIKeyRepository(db)
	key := &model.APIKey{
		KeyPrefix:       phase1RawKey[:16],
		KeyHash:         sha256Hex(phase1RawKey),
		AgentName:       "e2e-agent",
		OwnerUserID:     phase1OwnerID,
		PermissionScope: model.StringArray{"caasm:view_assets"},
		Status:          model.APIKeyStatusActive,
	}
	require.NoError(t, keyRepo.Create(context.Background(), key))
	require.NotZero(t, key.ID)

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

	// userRepo=nil：audit.Service 跳过用户存在性解析、直接保留归因 id；
	// employeeRepo=nil：本测试不用 List 的员工信息补全。Log 为同步写库（service.go:133）。
	auditSvc := audit.NewService(repository.NewAuditRepository(db), nil, nil, 0, nil)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = auditSvc.Shutdown(ctx)
	})

	// 最小 gin engine：与 router.go 的生产装配同序
	// CombinedAuth → AgentRouteAllowlist → AuditTrail → Permission(路由级)。
	engine := gin.New()
	api := engine.Group("/api/v1")
	api.Use(middleware.CombinedAuth(
		middleware.AgentAuth(keyRepo, config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-"}),
		middleware.Auth(jwtMgr),
	))
	api.Use(middleware.AgentRouteAllowlist())
	api.Use(middleware.AuditTrail(auditSvc))

	api.GET("/caasm/assets", middleware.Permission("caasm:view_assets"), func(c *gin.Context) {
		response.Success(c, gin.H{"handler": "caasm_assets"})
	})
	api.GET("/users", middleware.Permission("auth:manage_users"), func(c *gin.Context) {
		response.Success(c, gin.H{"handler": "users"})
	})
	// 无权限码路由：只要求认证，复刻 router.go 中 caasmIngestGroup 的形态。
	api.POST("/caasm/ingest", func(c *gin.Context) {
		response.Success(c, gin.H{"handler": "caasm_ingest"})
	})

	return &phase1Suite{
		db:       db,
		engine:   engine,
		auditSvc: auditSvc,
		keyID:    key.ID,
		jwtMgr:   jwtMgr,
	}
}

type phase1Result struct {
	httpStatus int
	body       response.APIResponse
}

// request 发进程内请求并解析统一信封。response.Error 恒 HTTP 200，故返回值同时
// 暴露 httpStatus（只用于佐证信封约定）与 body（断言按 body code）。
func (s *phase1Suite) request(t *testing.T, method, path string, headers map[string]string) phase1Result {
	t.Helper()
	w := httptest.NewRecorder()
	req := httptest.NewRequest(method, path, nil)
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	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}
}

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

func TestPhase1Integration(t *testing.T) {
	s := newPhase1Suite(t)

	t.Run("1_valid_agent_key_reaches_whitelisted_handler", func(t *testing.T) {
		res := s.request(t, http.MethodGet, "/api/v1/caasm/assets", s.agentHeaders())
		require.Equal(t, http.StatusOK, res.httpStatus, "信封约定: 成功分支 HTTP 200")
		require.Equal(t, 0, res.body.Code, "body=%+v", res.body)
		data, ok := res.body.Data.(map[string]interface{})
		require.True(t, ok, "成功响应应携带 data, body=%+v", res.body)
		require.Equal(t, "caasm_assets", data["handler"], "下游 handler 未执行")
	})

	t.Run("2_agent_key_forbidden_on_users_route", func(t *testing.T) {
		res := s.request(t, http.MethodGet, "/api/v1/users", s.agentHeaders())
		require.Equal(t, http.StatusOK, res.httpStatus, "信封约定: 错误也返回 HTTP 200")
		require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
		// 生产链路顺序 CombinedAuth → AgentRouteAllowlist → AuditTrail → Permission：
		// /api/v1/users 不在 35 个白名单端点内，由路由白名单先行拦截（纵深防御）；
		// 即使放行到 Permission 层，scope 无 auth:manage_users 同样 403。两层都拒绝。
		require.Equal(t, "agent not allowed on this route", res.body.Message)
		require.Nil(t, res.body.Data, "被拦截时下游 handler 不应执行")
	})

	t.Run("3_agent_key_blocked_by_route_allowlist_on_ingest", func(t *testing.T) {
		res := s.request(t, http.MethodPost, "/api/v1/caasm/ingest", s.agentHeaders())
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, http.StatusForbidden, res.body.Code, "body=%+v", res.body)
		require.Equal(t, "agent not allowed on this route", res.body.Message)
		require.Nil(t, res.body.Data, "ingest handler 不应执行")
	})

	t.Run("5_audit_attribution_for_agent_calls", func(t *testing.T) {
		// audit.Service.Log 同步写库（sync write + 失败才回退异步队列），
		// 这里仍按任务要求做最多 2s 轮询，对异步回退分支保持健壮。
		var logs []model.AuditLog
		deadline := time.Now().Add(2 * time.Second)
		for {
			require.NoError(t, s.db.Where("actor_type = ?", "agent").Find(&logs).Error)
			if len(logs) > 0 {
				break
			}
			if time.Now().After(deadline) {
				t.Fatal("2s 内未产生 actor_type='agent' 的审计记录")
			}
			time.Sleep(50 * time.Millisecond)
		}

		entry := logs[0]
		require.Equal(t, "GET", entry.Action)
		require.Equal(t, "caasm", entry.ResourceType)
		require.NotNil(t, entry.ActorUserID, "agent 调用应归因到 key owner")
		require.Equal(t, uint64(phase1OwnerID), *entry.ActorUserID)
		require.Equal(t, float64(s.keyID), entry.Metadata["agent_id"], "metadata 应含 agent_id")
		require.Equal(t, "e2e-agent", entry.Metadata["agent_name"])
		require.Equal(t, float64(phase1OwnerID), entry.Metadata["delegated_user_id"])
		require.Equal(t, "agent", entry.Metadata["auth_method"])
	})

	t.Run("4_revoked_key_rejected", func(t *testing.T) {
		// repo 层 Revoke 属 Phase 2 未实现，验收按 plan §1.9 直接改库吊销。
		require.NoError(t, s.db.Exec(
			"UPDATE api_keys SET status = ? WHERE id = ?",
			string(model.APIKeyStatusRevoked), s.keyID,
		).Error)

		res := s.request(t, http.MethodGet, "/api/v1/caasm/assets", s.agentHeaders())
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, http.StatusUnauthorized, res.body.Code, "body=%+v", res.body)
		require.Equal(t, "api key revoked", res.body.Message)
	})

	t.Run("6_human_jwt_admin_still_passes", func(t *testing.T) {
		token, err := s.jwtMgr.GenerateAccessToken(
			phase1AdminID, "admin@example.com",
			[]string{"admin"}, []string{"auth:manage_users"},
		)
		require.NoError(t, err)

		res := s.request(t, http.MethodGet, "/api/v1/users", map[string]string{
			"Authorization": "Bearer " + token,
		})
		require.Equal(t, http.StatusOK, res.httpStatus)
		require.Equal(t, 0, res.body.Code, "人类 admin 路径不应受 agent 通道影响, body=%+v", res.body)
		data, ok := res.body.Data.(map[string]interface{})
		require.True(t, ok)
		require.Equal(t, "users", data["handler"])
	})
}
