package agent

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"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"
)

// fakeUsageStatsQuerier implements usageStatsQuerier with call recording, so
// tests can assert the key id and time window the handler passed down without
// needing a MySQL server (sqlite cannot run the JSON extraction syntax).
type fakeUsageStatsQuerier struct {
	called bool
	keyID  uint64
	since  time.Time

	stats *usageStats
	err   error
}

func (f *fakeUsageStatsQuerier) QueryUsageStats(_ context.Context, keyID uint64, since time.Time) (*usageStats, error) {
	f.called = true
	f.keyID = keyID
	f.since = since
	if f.err != nil {
		return nil, f.err
	}
	if f.stats == nil {
		return &usageStats{}, nil
	}
	return f.stats, nil
}

// newUsageTestEngine mounts the routes behind a claims-injecting middleware,
// with the usage querier swapped for the fake under test.
func newUsageTestEngine(repo repository.APIKeyRepository, querier usageStatsQuerier, claims *jwt.Claims) *gin.Engine {
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	h := New(repo, config.AgentConfig{Enabled: true, KeyPrefix: "sk-agent-"})
	h.usage = querier
	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 ownKey() *model.APIKey {
	return &model.APIKey{
		Base:      model.Base{ID: 42, CreatedAt: time.Now()},
		AgentName: "my-agent", OwnerUserID: 7, Status: model.APIKeyStatusActive,
	}
}

func TestUsageRegularUserCannotSeeOthersKey(t *testing.T) {
	repo := &spyAPIKeyRepo{} // owner-scoped lookup misses → (nil, nil)
	querier := &fakeUsageStatsQuerier{}
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

	code, _ := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	if code != http.StatusNotFound {
		t.Fatalf("body code = %d, want 404", code)
	}
	if !repo.findByIDForOwnerCalled {
		t.Fatal("FindByIDForOwner was not called")
	}
	if got := repo.findByIDForOwnerArgs; got != [2]uint64{42, 7} {
		t.Fatalf("FindByIDForOwner args = %v, want [42 7] (id, current user)", got)
	}
	if repo.findByIDCalled {
		t.Fatal("FindByID admin fallback must not be used by a regular user")
	}
	if querier.called {
		t.Fatal("usage querier must not run for a key the user does not own")
	}
}

func TestUsageAdminCanSeeOthersKeyUsage(t *testing.T) {
	lastUsed := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
	repo := &spyAPIKeyRepo{findByIDKey: ownKey()}
	querier := &fakeUsageStatsQuerier{stats: &usageStats{
		dailyCounts: []APIKeyUsageDailyCount{
			{Date: "2026-07-25", Count: 3},
			{Date: "2026-07-26", Count: 5},
		},
		resourceBreakdown: []APIKeyUsageResourceCount{
			{ResourceType: "vulnerabilities", Count: 6},
			{ResourceType: "assets", Count: 2},
		},
		totalCalls: 8,
		lastUsedAt: &lastUsed,
	}}
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 9, Roles: []string{"admin"}})

	code, data := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if !repo.findByIDCalled {
		t.Fatal("FindByID was not called for admin")
	}
	if repo.findByIDForOwnerCalled {
		t.Fatal("owner-scoped lookup must not be used by admin")
	}
	if !querier.called {
		t.Fatal("usage querier was not called")
	}
	if querier.keyID != 42 {
		t.Fatalf("querier keyID = %d, want 42", querier.keyID)
	}
	// Default window is 30 days.
	if delta := time.Since(querier.since); delta < 30*24*time.Hour-time.Minute || delta > 30*24*time.Hour+time.Minute {
		t.Fatalf("querier since = %v, want ~30 days ago (delta %v)", querier.since, delta)
	}

	// Response structure must match the APIKeyUsage dto json tags exactly —
	// the frontend (lib/api/api-keys.ts, P5-T7) consumes these field names.
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		t.Fatalf("decode usage data: %v", err)
	}
	for _, field := range []string{"daily_counts", "resource_breakdown", "total_calls", "last_used_at", "window_days"} {
		if _, ok := raw[field]; !ok {
			t.Fatalf("usage data missing field %q; keys = %v", field, raw)
		}
	}
	var usage APIKeyUsage
	if err := json.Unmarshal(data, &usage); err != nil {
		t.Fatalf("decode APIKeyUsage: %v", err)
	}
	if usage.WindowDays != 30 {
		t.Fatalf("window_days = %d, want 30 (default)", usage.WindowDays)
	}
	if usage.TotalCalls != 8 {
		t.Fatalf("total_calls = %d, want 8", usage.TotalCalls)
	}
	if len(usage.DailyCounts) != 2 || usage.DailyCounts[0].Date != "2026-07-25" || usage.DailyCounts[0].Count != 3 {
		t.Fatalf("daily_counts = %+v, want the two fake rows", usage.DailyCounts)
	}
	if len(usage.ResourceBreakdown) != 2 || usage.ResourceBreakdown[0].ResourceType != "vulnerabilities" || usage.ResourceBreakdown[0].Count != 6 {
		t.Fatalf("resource_breakdown = %+v, want the two fake rows", usage.ResourceBreakdown)
	}
	if usage.LastUsedAt == nil || !usage.LastUsedAt.Equal(lastUsed) {
		t.Fatalf("last_used_at = %v, want %v", usage.LastUsedAt, lastUsed)
	}
}

func TestUsageRegularUserSeesOwnKeyUsage(t *testing.T) {
	repo := &spyAPIKeyRepo{findByIDForOwnerKey: ownKey()}
	querier := &fakeUsageStatsQuerier{}
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

	code, _ := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if !repo.findByIDForOwnerCalled {
		t.Fatal("FindByIDForOwner was not called for a regular user")
	}
	if !querier.called || querier.keyID != 42 {
		t.Fatalf("querier called=%v keyID=%d, want called with 42", querier.called, querier.keyID)
	}
}

func TestUsageDaysParamSelectsWindow(t *testing.T) {
	repo := &spyAPIKeyRepo{findByIDForOwnerKey: ownKey()}
	querier := &fakeUsageStatsQuerier{}
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

	code, data := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage?days=7")
	if code != 0 {
		t.Fatalf("body code = %d, want 0 (success)", code)
	}
	if delta := time.Since(querier.since); delta < 7*24*time.Hour-time.Minute || delta > 7*24*time.Hour+time.Minute {
		t.Fatalf("querier since = %v, want ~7 days ago (delta %v)", querier.since, delta)
	}
	var usage APIKeyUsage
	if err := json.Unmarshal(data, &usage); err != nil {
		t.Fatalf("decode APIKeyUsage: %v", err)
	}
	if usage.WindowDays != 7 {
		t.Fatalf("window_days = %d, want 7", usage.WindowDays)
	}
}

func TestUsageInvalidDaysParamReturns400(t *testing.T) {
	for _, raw := range []string{"abc", "15", "90", "0", "-7"} {
		repo := &spyAPIKeyRepo{findByIDForOwnerKey: ownKey()}
		querier := &fakeUsageStatsQuerier{}
		engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

		code, _ := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage?days="+raw)
		if code != http.StatusBadRequest {
			t.Fatalf("days=%s: body code = %d, want 400", raw, code)
		}
		if querier.called {
			t.Fatalf("days=%s: querier must not run for an invalid window", raw)
		}
	}
}

func TestUsageUnauthenticatedReturns401(t *testing.T) {
	repo := &spyAPIKeyRepo{}
	querier := &fakeUsageStatsQuerier{}
	engine := newUsageTestEngine(repo, querier, nil)

	code, _ := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	if code != http.StatusUnauthorized {
		t.Fatalf("body code = %d, want 401", code)
	}
	if repo.findByIDCalled || repo.findByIDForOwnerCalled {
		t.Fatal("repository must not be called when unauthenticated")
	}
	if querier.called {
		t.Fatal("querier must not be called when unauthenticated")
	}
}

func TestUsageQueryErrorReturns500(t *testing.T) {
	repo := &spyAPIKeyRepo{findByIDForOwnerKey: ownKey()}
	querier := &fakeUsageStatsQuerier{err: errors.New("db down")}
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

	code, _ := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	if code != http.StatusInternalServerError {
		t.Fatalf("body code = %d, want 500", code)
	}
}

func TestUsageEmptyWindowSerializesEmptyArraysNotNull(t *testing.T) {
	repo := &spyAPIKeyRepo{findByIDForOwnerKey: ownKey()}
	querier := &fakeUsageStatsQuerier{} // no stats → zero value
	engine := newUsageTestEngine(repo, querier, &jwt.Claims{UserID: 7})

	code, data := doListGet(t, engine, "/api/v1/agent/api-keys/42/usage")
	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 usage data: %v", err)
	}
	if got := string(raw["daily_counts"]); got != "[]" {
		t.Fatalf("daily_counts = %s, want [] (empty array, not null)", got)
	}
	if got := string(raw["resource_breakdown"]); got != "[]" {
		t.Fatalf("resource_breakdown = %s, want [] (empty array, not null)", got)
	}
	if got := string(raw["last_used_at"]); got != "null" {
		t.Fatalf("last_used_at = %s, want null when the key was never used in the window", got)
	}
}
