package agent

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

	"github.com/gin-gonic/gin"

	"security-center-backend/internal/config"
	"security-center-backend/internal/model"
	"security-center-backend/internal/service/jwt"
)

// fakeWhoamiUserLookup records the lookup argument and returns the staged
// user/error, so tests can assert which user id the handler resolved and
// drive the degrade/error paths.
type fakeWhoamiUserLookup struct {
	user  *model.User
	err   error
	calls int
	gotID uint64
}

func (f *fakeWhoamiUserLookup) FindUserByID(_ context.Context, id uint64) (*model.User, error) {
	f.calls++
	f.gotID = id
	return f.user, f.err
}

// newWhoamiTestEngine mounts GET /api/v1/agent/whoami the way router.go
// does — on the protected chain, outside the /agent/api-keys group and
// without the agent:manage_own_keys Permission middleware. A non-nil
// claims simulates the authenticated context CombinedAuth would inject;
// nil claims simulates the unauthenticated path.
func newWhoamiTestEngine(h *Handler, claims *jwt.Claims) *gin.Engine {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	if claims != nil {
		engine.Use(func(c *gin.Context) {
			c.Set("current_user", claims)
			c.Next()
		})
	}
	engine.GET("/api/v1/agent/whoami", h.Whoami)
	return engine
}

// doWhoami issues the GET and returns the envelope code plus raw data.
func doWhoami(t *testing.T, engine *gin.Engine) (int, json.RawMessage) {
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/whoami", nil)
	w := httptest.NewRecorder()
	engine.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("http status = %d, want 200 (envelope convention); body=%s", w.Code, w.Body.String())
	}
	var body struct {
		Code int             `json:"code"`
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode API response: %v; body=%s", err, w.Body.String())
	}
	return body.Code, body.Data
}

func TestWhoamiAgentReturnsIdentityAndScope(t *testing.T) {
	empID := uint64(88)
	lookup := &fakeWhoamiUserLookup{user: &model.User{
		Base: model.Base{ID: 7}, Name: "User A", Email: "a@example.com", EmployeeID: &empID,
	}}
	h := New(&spyAPIKeyRepo{}, config.AgentConfig{Enabled: true})
	h.users = lookup
	engine := newWhoamiTestEngine(h, &jwt.Claims{
		UserID:      7,
		ActorType:   "agent",
		AgentID:     12,
		AgentName:   "sec-bot",
		Permissions: []string{"vuln:view_findings"},
	})

	code, data := doWhoami(t, engine)
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if lookup.calls != 1 || lookup.gotID != 7 {
		t.Fatalf("FindUserByID calls=%d id=%d, want exactly one lookup with the key owner's user id 7", lookup.calls, lookup.gotID)
	}

	var resp WhoamiResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		t.Fatalf("decode whoami data: %v", err)
	}
	if resp.ActorType != "agent" || resp.AgentID != 12 || resp.AgentName != "sec-bot" {
		t.Fatalf("agent fields = %+v, want actor_type=agent agent_id=12 agent_name=sec-bot", resp)
	}
	if resp.UserID != 7 || resp.Name != "User A" || resp.Email != "a@example.com" {
		t.Fatalf("user fields = %+v, want user_id=7 name/email from the users row", resp)
	}
	if resp.EmployeeID == nil || *resp.EmployeeID != 88 {
		t.Fatalf("employee_id = %v, want 88", resp.EmployeeID)
	}
	if len(resp.PermissionScope) != 1 || resp.PermissionScope[0] != "vuln:view_findings" {
		t.Fatalf("permission_scope = %v, want [vuln:view_findings]", resp.PermissionScope)
	}
}

func TestWhoamiHumanReturnsUserShape(t *testing.T) {
	lookup := &fakeWhoamiUserLookup{user: &model.User{
		Base: model.Base{ID: 7}, Name: "User A", Email: "a@example.com", // 未关联员工: EmployeeID nil
	}}
	h := New(&spyAPIKeyRepo{}, config.AgentConfig{Enabled: true})
	h.users = lookup
	engine := newWhoamiTestEngine(h, &jwt.Claims{UserID: 7, Email: "a@example.com"})

	code, data := doWhoami(t, engine)
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		t.Fatalf("decode whoami data: %v", err)
	}
	var actorType string
	if err := json.Unmarshal(raw["actor_type"], &actorType); err != nil || actorType != "user" {
		t.Fatalf("actor_type = %q, want user (data=%s)", actorType, data)
	}
	for _, agentOnly := range []string{"agent_id", "agent_name", "permission_scope"} {
		if _, present := raw[agentOnly]; present {
			t.Fatalf("human response must omit %q (data=%s)", agentOnly, data)
		}
	}
	if string(raw["employee_id"]) != "null" {
		t.Fatalf("employee_id = %s, want explicit null for an unlinked user", raw["employee_id"])
	}
}

func TestWhoamiUnauthenticatedReturns401(t *testing.T) {
	lookup := &fakeWhoamiUserLookup{}
	h := New(&spyAPIKeyRepo{}, config.AgentConfig{Enabled: true})
	h.users = lookup
	engine := newWhoamiTestEngine(h, nil)

	code, _ := doWhoami(t, engine)
	if code != http.StatusUnauthorized {
		t.Fatalf("body code = %d, want 401", code)
	}
	if lookup.calls != 0 {
		t.Fatal("user lookup must not run without authenticated claims")
	}
}

func TestWhoamiWithoutUserLookupDegradesToClaims(t *testing.T) {
	// 无 db 的测试构造: h.users 为 nil, 只回 claims 字段 (employee_id=null)。
	h := New(&spyAPIKeyRepo{}, config.AgentConfig{Enabled: true})
	engine := newWhoamiTestEngine(h, &jwt.Claims{
		UserID:      7,
		ActorType:   "agent",
		AgentID:     12,
		AgentName:   "sec-bot",
		Permissions: []string{"vuln:view_findings"},
	})

	code, data := doWhoami(t, engine)
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	var resp WhoamiResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		t.Fatalf("decode whoami data: %v", err)
	}
	if resp.ActorType != "agent" || resp.AgentID != 12 || resp.UserID != 7 {
		t.Fatalf("claims fields = %+v, want actor_type=agent agent_id=12 user_id=7", resp)
	}
	if resp.Name != "" || resp.Email != "" || resp.EmployeeID != nil {
		t.Fatalf("db fields = %+v, want them empty in degrade mode", resp)
	}
}

func TestWhoamiUserLookupErrorReturns500(t *testing.T) {
	lookup := &fakeWhoamiUserLookup{err: errors.New("connection refused")}
	h := New(&spyAPIKeyRepo{}, config.AgentConfig{Enabled: true})
	h.users = lookup
	engine := newWhoamiTestEngine(h, &jwt.Claims{UserID: 7, Email: "a@example.com"})

	code, _ := doWhoami(t, engine)
	if code != http.StatusInternalServerError {
		t.Fatalf("body code = %d, want 500 on a real lookup failure", code)
	}
}
