package api

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"

	"account/internal/agentserver"
	"account/internal/store"
)

func TestAgentUsersUseAccountEmailAsStatsEmail(t *testing.T) {
	gin.SetMode(gin.TestMode)

	st := store.NewMemoryStore()
	ctx := context.Background()

	if err := st.CreateUser(ctx, &store.User{
		Name:          "Stats User",
		Email:         "stats@example.com",
		PasswordHash:  "hashed",
		EmailVerified: true,
		Role:          store.RoleUser,
		Level:         store.LevelUser,
		Active:        true,
		ProxyUUID:     "proxy-user-id",
	}); err != nil {
		t.Fatalf("create user: %v", err)
	}

	user, err := st.GetUserByEmail(ctx, "stats@example.com")
	if err != nil {
		t.Fatalf("get user: %v", err)
	}

	registry, err := agentserver.NewRegistry(agentserver.Config{
		Credentials: []agentserver.Credential{{
			ID:    "*",
			Name:  "test-agent",
			Token: "agent-token",
		}},
	})
	if err != nil {
		t.Fatalf("new registry: %v", err)
	}

	router := gin.New()
	RegisterRoutes(router, WithStore(st), WithAgentRegistry(registry), WithEmailVerification(false))

	req := httptest.NewRequest(http.MethodGet, "/api/agent-server/v1/users", nil)
	req.Header.Set("Authorization", "Bearer agent-token")
	req.Header.Set("X-Agent-ID", "hk-xhttp.svc.plus")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
	}

	var payload struct {
		Clients []struct {
			ID    string `json:"id"`
			Email string `json:"email"`
		} `json:"clients"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
		t.Fatalf("decode payload: %v", err)
	}

	for _, client := range payload.Clients {
		if client.Email == strings.ToLower(strings.TrimSpace(user.Email)) {
			return
		}
	}

	t.Fatalf("expected stats email %q in payload, got %#v", user.Email, payload.Clients)
}

func TestAccountUsageAndPolicyEndpoints(t *testing.T) {
	gin.SetMode(gin.TestMode)

	st := store.NewMemoryStore()
	ctx := context.Background()

	if err := st.CreateUser(ctx, &store.User{
		Name:          "Billing User",
		Email:         "billing@example.com",
		PasswordHash:  "hashed",
		EmailVerified: true,
		Role:          store.RoleUser,
		Level:         store.LevelUser,
		Active:        true,
	}); err != nil {
		t.Fatalf("create user: %v", err)
	}

	user, err := st.GetUserByEmail(ctx, "billing@example.com")
	if err != nil {
		t.Fatalf("get user: %v", err)
	}

	sessionToken := "usage-session-token"
	if err := st.CreateSession(ctx, sessionToken, user.ID, time.Now().UTC().Add(time.Hour)); err != nil {
		t.Fatalf("create session: %v", err)
	}

	bucketStart := time.Date(2026, 4, 1, 10, 30, 0, 0, time.UTC)
	if err := st.UpsertTrafficMinuteBucket(ctx, &store.TrafficMinuteBucket{
		BucketStart:   bucketStart,
		NodeID:        "node-a",
		AccountUUID:   user.ID,
		Region:        "hk",
		LineCode:      "premium",
		UplinkBytes:   128,
		DownlinkBytes: 256,
		TotalBytes:    384,
		Multiplier:    1.5,
		RatingStatus:  store.RatingStatusRated,
	}); err != nil {
		t.Fatalf("upsert bucket: %v", err)
	}

	if err := st.UpsertAccountQuotaState(ctx, &store.AccountQuotaState{
		AccountUUID:            user.ID,
		RemainingIncludedQuota: 2048,
		CurrentBalance:         87.5,
		Arrears:                false,
		ThrottleState:          "normal",
		SuspendState:           "active",
		EffectiveAt:            time.Now().UTC(),
	}); err != nil {
		t.Fatalf("upsert quota state: %v", err)
	}

	if err := st.UpsertAccountBillingProfile(ctx, &store.AccountBillingProfile{
		AccountUUID:        user.ID,
		PackageName:        "starter",
		IncludedQuotaBytes: 4096,
		BasePricePerByte:   0.125,
		RegionMultiplier:   1.2,
		LineMultiplier:     1.5,
		PeakMultiplier:     1.0,
		OffPeakMultiplier:  1.0,
		PricingRuleVersion: "pricing-v1",
	}); err != nil {
		t.Fatalf("upsert billing profile: %v", err)
	}

	if err := st.InsertBillingLedgerEntry(ctx, &store.BillingLedgerEntry{
		ID:                 "ledger-1",
		AccountUUID:        user.ID,
		BucketStart:        bucketStart,
		BucketEnd:          bucketStart.Add(time.Minute),
		EntryType:          "traffic_charge",
		RatedBytes:         384,
		AmountDelta:        -1.25,
		BalanceAfter:       87.5,
		PricingRuleVersion: "pricing-v1",
	}); err != nil {
		t.Fatalf("insert billing ledger: %v", err)
	}

	if err := st.UpsertAccountPolicySnapshot(ctx, &store.AccountPolicySnapshot{
		AccountUUID:        user.ID,
		PolicyVersion:      "policy-v1",
		AuthState:          "active",
		RateProfile:        "standard",
		ConnProfile:        "standard",
		EligibleNodeGroups: []string{"hk-premium"},
		PreferredStrategy:  "ewma",
		DegradeMode:        "fallback",
		ExpiresAt:          time.Now().UTC().Add(5 * time.Minute),
	}); err != nil {
		t.Fatalf("upsert policy: %v", err)
	}

	router := gin.New()
	RegisterRoutes(router, WithStore(st), WithEmailVerification(false))

	req := httptest.NewRequest(http.MethodGet, "/api/account/usage/summary", nil)
	req.Header.Set("Authorization", "Bearer "+sessionToken)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("usage summary status: %d body=%s", rec.Code, rec.Body.String())
	}

	var usagePayload struct {
		AccountUUID    string `json:"accountUuid"`
		TotalBytes     int64  `json:"totalBytes"`
		SourceOfTruth  string `json:"sourceOfTruth"`
		BillingProfile struct {
			PackageName        string  `json:"packageName"`
			BasePricePerByte   float64 `json:"basePricePerByte"`
			RegionMultiplier   float64 `json:"regionMultiplier"`
			LineMultiplier     float64 `json:"lineMultiplier"`
			PricingRuleVersion string  `json:"pricingRuleVersion"`
		} `json:"billingProfile"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &usagePayload); err != nil {
		t.Fatalf("decode usage payload: %v", err)
	}
	if usagePayload.AccountUUID != user.ID {
		t.Fatalf("expected account uuid %q, got %q", user.ID, usagePayload.AccountUUID)
	}
	if usagePayload.TotalBytes != 384 {
		t.Fatalf("expected total bytes 384, got %d", usagePayload.TotalBytes)
	}
	if usagePayload.SourceOfTruth != "postgresql" {
		t.Fatalf("expected source of truth postgresql, got %q", usagePayload.SourceOfTruth)
	}
	if usagePayload.BillingProfile.PackageName != "starter" {
		t.Fatalf("expected billing profile package starter, got %q", usagePayload.BillingProfile.PackageName)
	}

	bucketsReq := httptest.NewRequest(http.MethodGet, "/api/account/usage/buckets", nil)
	bucketsReq.Header.Set("Authorization", "Bearer "+sessionToken)
	bucketsRec := httptest.NewRecorder()
	router.ServeHTTP(bucketsRec, bucketsReq)

	if bucketsRec.Code != http.StatusOK {
		t.Fatalf("usage buckets status: %d body=%s", bucketsRec.Code, bucketsRec.Body.String())
	}

	var bucketsPayload struct {
		AccountUUID   string `json:"accountUuid"`
		SourceOfTruth string `json:"sourceOfTruth"`
		Buckets       []struct {
			TotalBytes  int64     `json:"totalBytes"`
			NodeID      string    `json:"nodeId"`
			BucketStart time.Time `json:"bucketStart"`
		} `json:"buckets"`
	}
	if err := json.Unmarshal(bucketsRec.Body.Bytes(), &bucketsPayload); err != nil {
		t.Fatalf("decode usage buckets payload: %v", err)
	}
	if bucketsPayload.AccountUUID != user.ID {
		t.Fatalf("expected usage buckets account uuid %q, got %q", user.ID, bucketsPayload.AccountUUID)
	}
	if bucketsPayload.SourceOfTruth != "postgresql" {
		t.Fatalf("expected usage buckets source of truth postgresql, got %q", bucketsPayload.SourceOfTruth)
	}
	if len(bucketsPayload.Buckets) != 1 {
		t.Fatalf("expected 1 usage bucket, got %d", len(bucketsPayload.Buckets))
	}
	if bucketsPayload.Buckets[0].TotalBytes != 384 {
		t.Fatalf("expected usage bucket total bytes 384, got %d", bucketsPayload.Buckets[0].TotalBytes)
	}
	if bucketsPayload.Buckets[0].NodeID != "node-a" {
		t.Fatalf("expected usage bucket node node-a, got %q", bucketsPayload.Buckets[0].NodeID)
	}

	billingReq := httptest.NewRequest(http.MethodGet, "/api/account/billing/summary", nil)
	billingReq.Header.Set("Authorization", "Bearer "+sessionToken)
	billingRec := httptest.NewRecorder()
	router.ServeHTTP(billingRec, billingReq)

	if billingRec.Code != http.StatusOK {
		t.Fatalf("billing summary status: %d body=%s", billingRec.Code, billingRec.Body.String())
	}

	var billingPayload struct {
		AccountUUID   string `json:"accountUuid"`
		SourceOfTruth string `json:"sourceOfTruth"`
		QuotaState    struct {
			CurrentBalance float64 `json:"currentBalance"`
		} `json:"quotaState"`
		BillingProfile struct {
			PackageName        string  `json:"packageName"`
			IncludedQuotaBytes int64   `json:"includedQuotaBytes"`
			BasePricePerByte   float64 `json:"basePricePerByte"`
		} `json:"billingProfile"`
		Ledger []struct {
			ID          string  `json:"id"`
			EntryType   string  `json:"entryType"`
			RatedBytes  int64   `json:"ratedBytes"`
			AmountDelta float64 `json:"amountDelta"`
		} `json:"ledger"`
	}
	if err := json.Unmarshal(billingRec.Body.Bytes(), &billingPayload); err != nil {
		t.Fatalf("decode billing payload: %v", err)
	}
	if billingPayload.AccountUUID != user.ID {
		t.Fatalf("expected billing account uuid %q, got %q", user.ID, billingPayload.AccountUUID)
	}
	if billingPayload.SourceOfTruth != "postgresql" {
		t.Fatalf("expected billing source of truth postgresql, got %q", billingPayload.SourceOfTruth)
	}
	if billingPayload.BillingProfile.IncludedQuotaBytes != 4096 {
		t.Fatalf("expected billing profile included quota 4096, got %d", billingPayload.BillingProfile.IncludedQuotaBytes)
	}
	if billingPayload.QuotaState.CurrentBalance != 87.5 {
		t.Fatalf("expected billing current balance 87.5, got %v", billingPayload.QuotaState.CurrentBalance)
	}
	if len(billingPayload.Ledger) != 1 {
		t.Fatalf("expected 1 billing ledger entry, got %d", len(billingPayload.Ledger))
	}
	if billingPayload.Ledger[0].ID != "ledger-1" {
		t.Fatalf("expected billing ledger id ledger-1, got %q", billingPayload.Ledger[0].ID)
	}
	if billingPayload.Ledger[0].EntryType != "traffic_charge" {
		t.Fatalf("expected billing entry type traffic_charge, got %q", billingPayload.Ledger[0].EntryType)
	}
	if billingPayload.Ledger[0].RatedBytes != 384 {
		t.Fatalf("expected billing rated bytes 384, got %d", billingPayload.Ledger[0].RatedBytes)
	}

	policyReq := httptest.NewRequest(http.MethodGet, "/api/account/policy", nil)
	policyReq.Header.Set("Authorization", "Bearer "+sessionToken)
	policyRec := httptest.NewRecorder()
	router.ServeHTTP(policyRec, policyReq)

	if policyRec.Code != http.StatusOK {
		t.Fatalf("policy status: %d body=%s", policyRec.Code, policyRec.Body.String())
	}

	var policyPayload struct {
		AccountUUID        string   `json:"accountUuid"`
		PreferredStrategy  string   `json:"preferredStrategy"`
		EligibleNodeGroups []string `json:"eligibleNodeGroups"`
	}
	if err := json.Unmarshal(policyRec.Body.Bytes(), &policyPayload); err != nil {
		t.Fatalf("decode policy payload: %v", err)
	}
	if policyPayload.AccountUUID != user.ID {
		t.Fatalf("expected policy account uuid %q, got %q", user.ID, policyPayload.AccountUUID)
	}
	if policyPayload.PreferredStrategy != "ewma" {
		t.Fatalf("expected preferred strategy ewma, got %q", policyPayload.PreferredStrategy)
	}
	if len(policyPayload.EligibleNodeGroups) != 1 || policyPayload.EligibleNodeGroups[0] != "hk-premium" {
		t.Fatalf("unexpected eligible node groups %#v", policyPayload.EligibleNodeGroups)
	}
}

func TestInternalNetworkIdentitiesEndpoint(t *testing.T) {
	gin.SetMode(gin.TestMode)

	t.Setenv("INTERNAL_SERVICE_TOKEN", "test-internal-token")

	st := store.NewMemoryStore()
	ctx := context.Background()

	if err := st.CreateUser(ctx, &store.User{
		Name:          "Exporter User",
		Email:         "exporter@example.com",
		PasswordHash:  "hashed",
		EmailVerified: true,
		Role:          store.RoleUser,
		Level:         store.LevelUser,
		Active:        true,
		ProxyUUID:     "proxy-exporter-id",
	}); err != nil {
		t.Fatalf("create user: %v", err)
	}

	if err := st.CreateUser(ctx, &store.User{
		Name:          "Inactive User",
		Email:         "inactive@example.com",
		PasswordHash:  "hashed",
		EmailVerified: true,
		Role:          store.RoleUser,
		Level:         store.LevelUser,
		Active:        false,
		ProxyUUID:     "proxy-inactive-id",
	}); err != nil {
		t.Fatalf("create inactive user: %v", err)
	}

	router := gin.New()
	RegisterRoutes(router, WithStore(st), WithEmailVerification(false))

	req := httptest.NewRequest(http.MethodGet, "/api/internal/network/identities", nil)
	req.Header.Set("X-Service-Token", os.Getenv("INTERNAL_SERVICE_TOKEN"))
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("internal identities status: %d body=%s", rec.Code, rec.Body.String())
	}

	var payload struct {
		Identities []struct {
			UUID        string `json:"uuid"`
			Email       string `json:"email"`
			AccountUUID string `json:"accountUuid"`
		} `json:"identities"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
		t.Fatalf("decode internal identities payload: %v", err)
	}

	foundExporter := false
	for _, identity := range payload.Identities {
		if identity.Email != "exporter@example.com" {
			continue
		}
		foundExporter = true
		if identity.UUID == "" {
			t.Fatalf("expected exporter UUID to be populated")
		}
		if identity.AccountUUID == "" {
			t.Fatalf("expected account uuid to be populated")
		}
	}
	if !foundExporter {
		t.Fatalf("expected exporter identity in payload, got %#v", payload.Identities)
	}
}
