package agent

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"

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

// fakeAPIKeyRepo captures the record handed to Create; the remaining
// repository methods are no-op stubs only needed to satisfy the interface.
type fakeAPIKeyRepo struct {
	created *model.APIKey
	err     error
}

func (f *fakeAPIKeyRepo) Create(_ context.Context, key *model.APIKey) error {
	if f.err != nil {
		return f.err
	}
	key.ID = 42
	key.CreatedAt = time.Now()
	f.created = key
	return nil
}

func (f *fakeAPIKeyRepo) FindByHash(context.Context, string) (*model.APIKey, error) {
	return nil, errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) TouchLastUsed(context.Context, uint64) error {
	return errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) FindByIDForOwner(context.Context, uint64, uint64) (*model.APIKey, error) {
	return nil, errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) ListByOwner(context.Context, uint64) ([]*model.APIKey, error) {
	return nil, errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) Revoke(context.Context, uint64, uint64) error {
	return errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) FindByID(context.Context, uint64) (*model.APIKey, error) {
	return nil, errors.New("not implemented")
}

func (f *fakeAPIKeyRepo) ListAll(context.Context, repository.ListAPIKeysFilter) ([]*model.APIKey, error) {
	return nil, errors.New("not implemented")
}

// injectClaims simulates the auth middleware: non-nil claims are placed into
// the context exactly like the real JWT middleware does.
func injectClaims(claims *jwt.Claims) gin.HandlerFunc {
	return func(c *gin.Context) {
		if claims != nil {
			c.Set("current_user", claims)
		}
		c.Next()
	}
}

func newCreateTestEngine(repo *fakeAPIKeyRepo, cfg config.AgentConfig, claims *jwt.Claims) *gin.Engine {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	h := New(repo, cfg)
	engine.POST("/api/v1/agent/api-keys", injectClaims(claims), h.Create)
	return engine
}

func doCreateRequest(t *testing.T, engine *gin.Engine, body string) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/api-keys", bytes.NewBufferString(body))
	req.Header.Set("Content-Type", "application/json")
	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())
	}
	return w
}

func decodeCreateEnvelope(t *testing.T, w *httptest.ResponseRecorder) (int, *CreateAPIKeyResponse) {
	t.Helper()
	var envelope struct {
		Code int                   `json:"code"`
		Data *CreateAPIKeyResponse `json:"data"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode API response: %v; body=%s", err, w.Body.String())
	}
	return envelope.Code, envelope.Data
}

var createTestClaims = &jwt.Claims{
	UserID:      7,
	Email:       "owner@example.com",
	Permissions: []string{"agent:manage_own_keys", "caasm:view_assets", "vuln:view_findings"},
}

func TestCreateSuccess(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	expires := time.Now().Add(72 * time.Hour).UTC().Truncate(time.Second)
	body := fmt.Sprintf(`{"agent_name":"My Agent!! Bot","permission_scope":["caasm:view_assets"],"expires_at":%q}`, expires.Format(time.RFC3339))
	w := doCreateRequest(t, engine, body)

	code, data := decodeCreateEnvelope(t, w)
	if code != 0 {
		t.Fatalf("body code = %d, want 0; body=%s", code, w.Body.String())
	}
	if repo.created == nil {
		t.Fatal("repo.Create was not called")
	}
	stored := repo.created

	// owner 强制为当前用户
	if stored.OwnerUserID != createTestClaims.UserID {
		t.Errorf("OwnerUserID = %d, want %d", stored.OwnerUserID, createTestClaims.UserID)
	}
	// 明文只在响应体出现, 落库的是 sha256 摘要
	if data == nil || data.APIKey == "" {
		t.Fatalf("response missing plaintext api_key; body=%s", w.Body.String())
	}
	wantHash := fmt.Sprintf("%x", sha256.Sum256([]byte(data.APIKey)))
	if stored.KeyHash != wantHash {
		t.Errorf("KeyHash = %q, want sha256(plaintext) = %q", stored.KeyHash, wantHash)
	}
	if stored.KeyHash == data.APIKey {
		t.Error("plaintext key was persisted in KeyHash")
	}
	// KeyPrefix 为明文前 16 字符
	if stored.KeyPrefix != data.APIKey[:16] {
		t.Errorf("KeyPrefix = %q, want plaintext[:16] = %q", stored.KeyPrefix, data.APIKey[:16])
	}
	if data.KeyPrefix != data.APIKey[:16] {
		t.Errorf("response key_prefix = %q, want %q", data.KeyPrefix, data.APIKey[:16])
	}
	// 明文格式: prefix + slug + "-" + 32 hex
	if !strings.HasPrefix(data.APIKey, "sk-agent-my-agent-bot-") {
		t.Errorf("api_key = %q, want prefix %q", data.APIKey, "sk-agent-my-agent-bot-")
	}
	if len(data.APIKey) != len("sk-agent-my-agent-bot-")+32 {
		t.Errorf("api_key length = %d, want %d", len(data.APIKey), len("sk-agent-my-agent-bot-")+32)
	}
	if stored.Status != model.APIKeyStatusActive {
		t.Errorf("Status = %q, want %q", stored.Status, model.APIKeyStatusActive)
	}
	if data.Status != string(model.APIKeyStatusActive) {
		t.Errorf("response status = %q, want %q", data.Status, model.APIKeyStatusActive)
	}
	if stored.AgentName != "My Agent!! Bot" {
		t.Errorf("AgentName = %q, want %q", stored.AgentName, "My Agent!! Bot")
	}
	if len(stored.PermissionScope) != 1 || stored.PermissionScope[0] != "caasm:view_assets" {
		t.Errorf("PermissionScope = %v, want [caasm:view_assets]", stored.PermissionScope)
	}
	if stored.ExpiresAt == nil || !stored.ExpiresAt.Equal(expires) {
		t.Errorf("ExpiresAt = %v, want %v", stored.ExpiresAt, expires)
	}
	if data.ID != 42 {
		t.Errorf("response id = %d, want 42", data.ID)
	}
}

func TestCreatePermanentKeyAllowed(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	w := doCreateRequest(t, engine, `{"agent_name":"bot","permission_scope":["caasm:view_assets"]}`)
	code, data := decodeCreateEnvelope(t, w)
	if code != 0 {
		t.Fatalf("body code = %d, want 0; body=%s", code, w.Body.String())
	}
	if repo.created == nil || repo.created.ExpiresAt != nil {
		t.Fatalf("ExpiresAt = %v, want nil (permanent key)", repo.created.ExpiresAt)
	}
	// 空 KeyPrefix 兜底为 sk-agent-
	if !strings.HasPrefix(data.APIKey, "sk-agent-bot-") {
		t.Errorf("api_key = %q, want prefix %q", data.APIKey, "sk-agent-bot-")
	}
}

func TestCreateScopeForbidden(t *testing.T) {
	tests := []struct {
		name      string
		reqScope  []string
		userPerms []string
	}{
		{
			"user lacks requested permission",
			[]string{"vuln:view_findings"},
			[]string{"caasm:view_assets"},
		},
		{
			"write perm outside read-only whitelist",
			[]string{"caasm:manage_assets"},
			[]string{"caasm:manage_assets"},
		},
		{
			"denylist perm rejected even if user has it",
			[]string{"vuln:view_sensitive_details"},
			[]string{"vuln:view_sensitive_details"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo := &fakeAPIKeyRepo{}
			cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
			claims := &jwt.Claims{UserID: 7, Permissions: tt.userPerms}
			engine := newCreateTestEngine(repo, cfg, claims)

			scope, _ := json.Marshal(tt.reqScope)
			body := fmt.Sprintf(`{"agent_name":"bot","permission_scope":%s}`, scope)
			w := doCreateRequest(t, engine, body)

			code, _ := decodeCreateEnvelope(t, w)
			if code != http.StatusForbidden {
				t.Fatalf("body code = %d, want 403; body=%s", code, w.Body.String())
			}
			if repo.created != nil {
				t.Error("repo.Create must not be called on scope violation")
			}
		})
	}
}

func TestCreateExpiresAtRequired(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: false}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	w := doCreateRequest(t, engine, `{"agent_name":"bot","permission_scope":["caasm:view_assets"]}`)
	code, _ := decodeCreateEnvelope(t, w)
	if code != http.StatusBadRequest {
		t.Fatalf("body code = %d, want 400; body=%s", code, w.Body.String())
	}
	if repo.created != nil {
		t.Error("repo.Create must not be called when expires_at is missing")
	}
}

func TestCreateExpiresAtInPast(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	past := time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)
	body := fmt.Sprintf(`{"agent_name":"bot","permission_scope":["caasm:view_assets"],"expires_at":%q}`, past)
	w := doCreateRequest(t, engine, body)

	code, _ := decodeCreateEnvelope(t, w)
	if code != http.StatusBadRequest {
		t.Fatalf("body code = %d, want 400; body=%s", code, w.Body.String())
	}
	if repo.created != nil {
		t.Error("repo.Create must not be called for a past expires_at")
	}
}

func TestCreateMissingAgentName(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	w := doCreateRequest(t, engine, `{"permission_scope":["caasm:view_assets"]}`)
	code, _ := decodeCreateEnvelope(t, w)
	if code != http.StatusBadRequest {
		t.Fatalf("body code = %d, want 400; body=%s", code, w.Body.String())
	}
	if repo.created != nil {
		t.Error("repo.Create must not be called when agent_name is missing")
	}
}

func TestCreateIgnoresOwnerUserID(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, createTestClaims)

	body := `{"agent_name":"bot","permission_scope":["caasm:view_assets"],"owner_user_id":999}`
	w := doCreateRequest(t, engine, body)

	code, _ := decodeCreateEnvelope(t, w)
	if code != 0 {
		t.Fatalf("body code = %d, want 0; body=%s", code, w.Body.String())
	}
	if repo.created == nil || repo.created.OwnerUserID != createTestClaims.UserID {
		t.Fatalf("OwnerUserID = %v, want %d (request owner_user_id must be ignored)",
			repo.created, createTestClaims.UserID)
	}
}

func TestCreateUnauthenticated(t *testing.T) {
	repo := &fakeAPIKeyRepo{}
	cfg := config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-", AllowPermanentKey: true}
	engine := newCreateTestEngine(repo, cfg, nil)

	w := doCreateRequest(t, engine, `{"agent_name":"bot","permission_scope":["caasm:view_assets"]}`)
	code, _ := decodeCreateEnvelope(t, w)
	if code != http.StatusUnauthorized {
		t.Fatalf("body code = %d, want 401; body=%s", code, w.Body.String())
	}
	if repo.created != nil {
		t.Error("repo.Create must not be called without authenticated claims")
	}
}

func TestSlugAgentName(t *testing.T) {
	tests := []struct{ name, want string }{
		{"My Agent!! Bot", "my-agent-bot"},
		{"!!!", "agent"},
		{"", "agent"},
		{"already-slug", "already-slug"},
		{"--Lead  and--Trail--", "lead-and-trail"},
		{"A very long agent name that exceeds the cap", "a-very-long-agent-na"},
	}
	for _, tt := range tests {
		if got := slugAgentName(tt.name); got != tt.want {
			t.Errorf("slugAgentName(%q) = %q, want %q", tt.name, got, tt.want)
		}
		if got := slugAgentName(tt.name); len(got) > 20 {
			t.Errorf("slugAgentName(%q) length = %d, want <= 20", tt.name, len(got))
		}
	}
}
