package agent

import (
	"context"
	"encoding/json"
	"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/repository"
	"security-center-backend/internal/service/jwt"
)

// revokeFakeRepo is an in-memory APIKeyRepository for the Revoke tests.
// Revoke mirrors the real implementation: it scopes the update by owner and
// returns ErrAPIKeyNotFoundOrNotOwned when no row matches both id and owner.
// (Named distinctly from create_test.go's fakeAPIKeyRepo, which stubs Revoke.)
type revokeFakeRepo struct {
	keys map[uint64]*model.APIKey
}

func (f *revokeFakeRepo) Create(context.Context, *model.APIKey) error { return nil }
func (f *revokeFakeRepo) FindByHash(context.Context, string) (*model.APIKey, error) {
	return nil, nil
}
func (f *revokeFakeRepo) TouchLastUsed(context.Context, uint64) error { return nil }
func (f *revokeFakeRepo) ListByOwner(context.Context, uint64) ([]*model.APIKey, error) {
	return nil, nil
}
func (f *revokeFakeRepo) ListAll(context.Context, repository.ListAPIKeysFilter) ([]*model.APIKey, error) {
	return nil, nil
}

func (f *revokeFakeRepo) FindByIDForOwner(_ context.Context, id, ownerUserID uint64) (*model.APIKey, error) {
	key, ok := f.keys[id]
	if !ok || key.OwnerUserID != ownerUserID {
		return nil, nil
	}
	return key, nil
}

func (f *revokeFakeRepo) FindByID(_ context.Context, id uint64) (*model.APIKey, error) {
	key, ok := f.keys[id]
	if !ok {
		return nil, nil
	}
	return key, nil
}

func (f *revokeFakeRepo) Revoke(_ context.Context, id, ownerUserID uint64) error {
	key, ok := f.keys[id]
	if !ok || key.OwnerUserID != ownerUserID {
		return repository.ErrAPIKeyNotFoundOrNotOwned
	}
	key.Status = model.APIKeyStatusRevoked
	return nil
}

// newRevokeTestEngine mounts the routes behind a middleware that injects the
// given claims, simulating the authenticated context the JWT middleware
// would produce.
func newRevokeTestEngine(repo repository.APIKeyRepository, claims *jwt.Claims) *gin.Engine {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	h := New(repo, config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-"})
	group := engine.Group("/api/v1/agent/api-keys")
	group.Use(func(c *gin.Context) {
		c.Set("current_user", claims)
		c.Next()
	})
	h.RegisterRoutes(group)
	return engine
}

func doRevoke(t *testing.T, engine *gin.Engine, id string) (int, *APIKeyResponse) {
	t.Helper()
	req := httptest.NewRequest(http.MethodDelete, "/api/v1/agent/api-keys/"+id, 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 *APIKeyResponse `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 TestRevokeOwnKeySucceeds(t *testing.T) {
	repo := &revokeFakeRepo{keys: map[uint64]*model.APIKey{
		1: {Base: model.Base{ID: 1}, AgentName: "my-agent", OwnerUserID: 7, Status: model.APIKeyStatusActive},
	}}
	engine := newRevokeTestEngine(repo, &jwt.Claims{UserID: 7})

	code, data := doRevoke(t, engine, "1")
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if data == nil || data.Status != string(model.APIKeyStatusRevoked) {
		t.Fatalf("data = %+v, want status=revoked", data)
	}
	if repo.keys[1].Status != model.APIKeyStatusRevoked {
		t.Fatalf("repo status = %q, want revoked", repo.keys[1].Status)
	}
}

func TestRevokeOthersKeyReturns404(t *testing.T) {
	// 吊销他人的 key 与吊销不存在的 key 返回同样的 404，不区分以防探测。
	repo := &revokeFakeRepo{keys: map[uint64]*model.APIKey{
		1: {Base: model.Base{ID: 1}, AgentName: "foreign-agent", OwnerUserID: 8, Status: model.APIKeyStatusActive},
	}}
	engine := newRevokeTestEngine(repo, &jwt.Claims{UserID: 7})

	code, _ := doRevoke(t, engine, "1")
	if code != http.StatusNotFound {
		t.Fatalf("body code = %d, want 404", code)
	}
	if repo.keys[1].Status != model.APIKeyStatusActive {
		t.Fatalf("foreign key status changed to %q, want still active", repo.keys[1].Status)
	}
}

func TestRevokeAsAdminCanRevokeOthersKey(t *testing.T) {
	repo := &revokeFakeRepo{keys: map[uint64]*model.APIKey{
		1: {Base: model.Base{ID: 1}, AgentName: "foreign-agent", OwnerUserID: 8, Status: model.APIKeyStatusActive},
	}}
	engine := newRevokeTestEngine(repo, &jwt.Claims{UserID: 7, Roles: []string{"admin"}})

	code, data := doRevoke(t, engine, "1")
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if data == nil || data.Status != string(model.APIKeyStatusRevoked) {
		t.Fatalf("data = %+v, want status=revoked", data)
	}
	if repo.keys[1].Status != model.APIKeyStatusRevoked {
		t.Fatalf("repo status = %q, want revoked", repo.keys[1].Status)
	}
}

func TestRevokeWithNonNumericIDReturns400(t *testing.T) {
	repo := &revokeFakeRepo{keys: map[uint64]*model.APIKey{}}
	engine := newRevokeTestEngine(repo, &jwt.Claims{UserID: 7})

	code, _ := doRevoke(t, engine, "abc")
	if code != http.StatusBadRequest {
		t.Fatalf("body code = %d, want 400", code)
	}
}

func TestRevokeNotFoundReturns404(t *testing.T) {
	repo := &revokeFakeRepo{keys: map[uint64]*model.APIKey{}}
	engine := newRevokeTestEngine(repo, &jwt.Claims{UserID: 7})

	code, _ := doRevoke(t, engine, "999")
	if code != http.StatusNotFound {
		t.Fatalf("body code = %d, want 404", code)
	}
}

// 联动说明：本测试只覆盖吊销动作本身。被吊销的 key 再次携带调用业务接口会
// 得到 401，该行为由 AgentAuth 中间件（校验 key status）保障，已在 Phase 1
// 的中间件测试中验证，此处不重复覆盖。
