package billing

import (
	"context"
	"crypto/sha256"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/billing/alipay"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
)

func init() {
	gin.SetMode(gin.TestMode)
}

func int64Ptr(value int64) *int64 {
	return &value
}

func TestCreateOrderRequiresOrgAdminAndSignsPagePay(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 9, 0, 0, 0, time.UTC)
	billingStore := &fakeBillingClient{
		createOrder: billingPaymentOrder{
			OrderID:            "pay_1",
			OrganizationID:     "org_1",
			WorkspaceID:        "wsp_1",
			CreatedByUserID:    "usr_1",
			OutTradeNo:         "agentteam_1",
			Status:             "pending",
			CreditUnit:         billingCreditUnitAgentTeamCredit,
			CreditAmount:       3000,
			Currency:           billingCurrencyCNY,
			PayableAmountCents: 3000,
			Provider:           billingProviderAlipay,
			ProviderMode:       billingProviderModePagePay,
			ExpiresAt:          now.Add(billingOrderTTL),
			CreatedAt:          now,
			UpdatedAt:          now,
		},
	}
	alipayClient := &fakeAlipayClient{pagePayURL: "https://openapi.alipay.test/pay"}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_admin",
		}},
		BillingClient: billingStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/credit-recharge-orders", strings.NewReader(`{"creditAmount":3000}`))
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")
	request.Header.Set("Idempotency-Key", "idem_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusCreated, recorder.Code)
	require.Equal(t, "org_1", billingStore.created.OrganizationID)
	require.Equal(t, "wsp_1", billingStore.created.WorkspaceID)
	require.Equal(t, "usr_1", billingStore.created.CreatedByUserID)
	require.Equal(t, "idem_1", billingStore.created.IdempotencyKey)
	require.Equal(t, int64(3000), billingStore.created.PayableAmountCents)
	require.Equal(t, "hosted-credit-atoms-v1", billingStore.created.PriceSnapshot["source"])
	require.Equal(t, billingCreditAmountPerCNY, billingStore.created.PriceSnapshot["creditAmountPerCny"])
	require.Equal(t, billingCreditAtomsPerCredit, billingStore.created.PriceSnapshot["creditAtomsPerCredit"])
	require.Equal(t, billingCreditAtomsPerCNY, billingStore.created.PriceSnapshot["creditAtomsPerCny"])
	require.Equal(t, now.Add(billingOrderTTL), billingStore.created.ExpiresAt)
	require.Equal(t, "agentteam_1", alipayClient.pagePayInput.OutTradeNo)
	require.Equal(t, "30.00", alipayClient.pagePayInput.Amount)
	require.Equal(t, "https://agentteam.test/api/v1/billing/alipay/notify", alipayClient.pagePayInput.NotifyURL)
	require.Contains(t, alipayClient.pagePayInput.ReturnURL, "workspace_id=wsp_1")
	require.Contains(t, alipayClient.pagePayInput.ReturnURL, "order_id=pay_1")
	require.JSONEq(t, `{"orderId":"pay_1","outTradeNo":"agentteam_1","status":"pending","creditUnit":"agentteam_credit","creditAmount":3000,"currency":"CNY","payableAmountCents":3000,"paymentUrl":"https://openapi.alipay.test/pay","expiresAt":"2026-06-03T09:30:00Z"}`, recorder.Body.String())
}

func TestSummaryRejectsNonOrganizationAdmin(t *testing.T) {
	t.Parallel()

	router := gin.New()
	billingStore := &fakeBillingClient{}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_member",
		}},
		BillingClient: billingStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/summary", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.False(t, billingStore.summaryCalled)
}

func TestSummaryReturnsNotFoundWhenOrganizationCannotBeResolvedForUser(t *testing.T) {
	t.Parallel()

	router := gin.New()
	billingStore := &fakeBillingClient{}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{
			err: organization.ErrForbidden,
		},
		BillingClient: billingStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/summary", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusNotFound, recorder.Code)
	require.False(t, billingStore.summaryCalled)
}

func TestSummaryReturnsUnavailableWhenOrganizationLookupFails(t *testing.T) {
	t.Parallel()

	router := gin.New()
	billingStore := &fakeBillingClient{}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{
			err: fmt.Errorf("database unavailable"),
		},
		BillingClient: billingStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/summary", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	require.False(t, billingStore.summaryCalled)
}

func TestPricingReturnsHostedModelPublicReferences(t *testing.T) {
	t.Parallel()

	router := gin.New()
	billingStore := &fakeBillingClient{
		pricing: billingPricing{
			CreditUnit:           billingCreditUnitAgentTeamCredit,
			Currency:             billingCurrencyCNY,
			CreditAmountPerCNY:   billingCreditAmountPerCNY,
			CreditAtomsPerCredit: billingCreditAtomsPerCredit,
			CreditAtomsPerCNY:    billingCreditAtomsPerCNY,
			RechargePresets:      []int64{3000, 5000, 10000},
			CustomMinCredits:     billingCustomMinCredits,
			HostedModels: []billingHostedModelPricing{
				{
					ID:                               "agentteam/deepseek-v4-pro",
					ModelReference:                   "agentteam/deepseek-v4-pro",
					HostedModelID:                    "deepseek_v4_pro",
					DisplayName:                      "DeepSeek V4 Pro",
					InputCreditAtomsPerMillionTokens: int64Ptr(2_000_000),
					CacheReadInputCreditAtomsPerMillionTokens: int64Ptr(200_000),
					OutputCreditAtomsPerMillionTokens:         int64Ptr(8_000_000),
				},
				{
					ID:                        "agentteam/context-tiered",
					ModelReference:            "agentteam/context-tiered",
					HostedModelID:             "context_tiered",
					DisplayName:               "Context Tiered",
					PricingVersionID:          "hpv_context_discount_90",
					EffectivePricingVersionID: "hpv_context_discount_90",
					PricingFormulaVersion:     "hosted_token_context_threshold_v1",
					BillableDimensions:        []string{"input_tokens", "cache_read_input_tokens", "output_tokens"},
					BasePricingVersionID:      "hpv_context_standard",
					BasePricingFormulaVersion: "hosted_token_context_threshold_v1",
					BaseBillableDimensions:    []string{"input_tokens", "cache_read_input_tokens", "output_tokens"},
					ContextTiers: []HostedPricingTier{
						{
							ID:               "short_context",
							MinContextTokens: 0,
							MaxContextTokens: int64Ptr(4096),
							Rates: HostedPricingTierRates{
								InputTokensCreditAtomsPerMillionTokens:    1_000_000,
								CacheReadInputCreditAtomsPerMillionTokens: 100_000,
								OutputCreditAtomsPerMillionTokens:         3_000_000,
							},
						},
						{
							ID:               "long_context",
							MinContextTokens: 4097,
							Rates: HostedPricingTierRates{
								InputTokensCreditAtomsPerMillionTokens:    2_000_000,
								CacheReadInputCreditAtomsPerMillionTokens: 200_000,
								OutputCreditAtomsPerMillionTokens:         6_000_000,
							},
						},
					},
					BaseContextTiers: []HostedPricingTier{
						{
							ID:               "short_context",
							MinContextTokens: 0,
							MaxContextTokens: int64Ptr(4096),
							Rates: HostedPricingTierRates{
								InputTokensCreditAtomsPerMillionTokens:    2_000_000,
								CacheReadInputCreditAtomsPerMillionTokens: 200_000,
								OutputCreditAtomsPerMillionTokens:         6_000_000,
							},
						},
						{
							ID:               "long_context",
							MinContextTokens: 4097,
							Rates: HostedPricingTierRates{
								InputTokensCreditAtomsPerMillionTokens:    4_000_000,
								CacheReadInputCreditAtomsPerMillionTokens: 400_000,
								OutputCreditAtomsPerMillionTokens:         12_000_000,
							},
						},
					},
					PricingOverride: &HostedPricingOverride{
						ID:                        "hpod_context_campaign",
						HostedModelID:             "context_tiered",
						ScopeType:                 "global",
						BasePricingVersionID:      "hpv_context_standard",
						EffectivePricingVersionID: "hpv_context_discount_90",
						OverrideType:              "campaign",
						DisplayLabel:              "90% campaign",
						Priority:                  100,
						Status:                    "active",
					},
				},
			},
		},
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		BillingClient: billingStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/pricing", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "org_1", billingStore.pricingOrganizationID)
	require.JSONEq(t, `{
		"creditUnit":"agentteam_credit",
		"currency":"CNY",
		"creditAmountPerCny":100,
		"creditAtomsPerCredit":1000000,
		"creditAtomsPerCny":100000000,
		"rechargePresets":[3000,5000,10000],
		"customMinCredits":100,
		"hostedModels":[{
			"id":"agentteam/deepseek-v4-pro",
			"modelReference":"agentteam/deepseek-v4-pro",
			"hostedModelId":"deepseek_v4_pro",
			"displayName":"DeepSeek V4 Pro",
			"inputCreditAtomsPerMillionTokens":2000000,
			"cacheReadInputCreditAtomsPerMillionTokens":200000,
			"outputCreditAtomsPerMillionTokens":8000000
		},{
			"id":"agentteam/context-tiered",
			"modelReference":"agentteam/context-tiered",
			"hostedModelId":"context_tiered",
			"displayName":"Context Tiered",
			"pricingVersionId":"hpv_context_discount_90",
			"effectivePricingVersionId":"hpv_context_discount_90",
			"pricingFormulaVersion":"hosted_token_context_threshold_v1",
			"billableDimensions":["input_tokens","cache_read_input_tokens","output_tokens"],
			"basePricingVersionId":"hpv_context_standard",
			"basePricingFormulaVersion":"hosted_token_context_threshold_v1",
			"baseBillableDimensions":["input_tokens","cache_read_input_tokens","output_tokens"],
			"contextTiers":[{
				"id":"short_context",
				"minContextTokens":0,
				"maxContextTokens":4096,
				"rates":{
					"inputTokensCreditAtomsPerMillionTokens":1000000,
					"cacheReadInputCreditAtomsPerMillionTokens":100000,
					"outputCreditAtomsPerMillionTokens":3000000
				}
			},{
				"id":"long_context",
				"minContextTokens":4097,
				"maxContextTokens":null,
				"rates":{
					"inputTokensCreditAtomsPerMillionTokens":2000000,
					"cacheReadInputCreditAtomsPerMillionTokens":200000,
					"outputCreditAtomsPerMillionTokens":6000000
				}
			}],
			"baseContextTiers":[{
				"id":"short_context",
				"minContextTokens":0,
				"maxContextTokens":4096,
				"rates":{
					"inputTokensCreditAtomsPerMillionTokens":2000000,
					"cacheReadInputCreditAtomsPerMillionTokens":200000,
					"outputCreditAtomsPerMillionTokens":6000000
				}
			},{
				"id":"long_context",
				"minContextTokens":4097,
				"maxContextTokens":null,
				"rates":{
					"inputTokensCreditAtomsPerMillionTokens":4000000,
					"cacheReadInputCreditAtomsPerMillionTokens":400000,
					"outputCreditAtomsPerMillionTokens":12000000
				}
			}],
			"pricingOverride":{
				"id":"hpod_context_campaign",
				"hostedModelId":"context_tiered",
				"scopeType":"global",
				"basePricingVersionId":"hpv_context_standard",
				"effectivePricingVersionId":"hpv_context_discount_90",
				"overrideType":"campaign",
				"displayLabel":"90% campaign",
				"priority":100,
				"status":"active"
			}
		}]
	}`, recorder.Body.String())
}

func TestAlipayNotifyConfirmsPaidOrder(t *testing.T) {
	t.Parallel()

	router := gin.New()
	paidAt := time.Date(2026, 6, 3, 1, 0, 0, 0, time.UTC)
	order := billingPaymentOrder{
		OrderID:            "pay_1",
		OrganizationID:     "org_1",
		OutTradeNo:         "agentteam_1",
		Status:             "pending",
		CreditUnit:         billingCreditUnitAgentTeamCredit,
		CreditAmount:       3000,
		Currency:           billingCurrencyCNY,
		PayableAmountCents: 3000,
		Provider:           billingProviderAlipay,
		ProviderMode:       billingProviderModePagePay,
	}
	billingStore := &fakeBillingClient{orderByOutTradeNo: order, confirmOrder: order}
	RegisterRoutes(router, Dependencies{
		SessionAuth:               fakeSessionAuth{userID: "usr_1"},
		Organizations:             fakeOrganizationProvider{},
		BillingClient:             billingStore,
		Alipay:                    &fakeAlipayClient{},
		Enabled:                   true,
		PublicBaseURL:             "https://agentteam.test",
		EmailVerificationRequired: false,
	})
	form := url.Values{
		"out_trade_no": {"agentteam_1"},
		"trade_no":     {"trade_1"},
		"trade_status": {alipayTradeStatusSuccess},
		"total_amount": {"30.00"},
		"gmt_payment":  {"2026-06-03 09:00:00"},
		"notify_time":  {"2026-06-03 09:00:01"},
		"sign":         {"signature"},
		"sign_type":    {"RSA2"},
	}
	raw := form.Encode()

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/alipay/notify", strings.NewReader(raw))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "success", recorder.Body.String())
	require.Equal(t, "pay_1", billingStore.confirmedOrderID)
	require.Equal(t, "org_1", billingStore.confirmed.OrganizationID)
	require.Equal(t, "notify", billingStore.confirmed.Source)
	require.Equal(t, "trade_1", billingStore.confirmed.ProviderTradeNo)
	require.Equal(t, int64(3000), billingStore.confirmed.PaidAmountCents)
	require.Equal(t, paidAt, billingStore.confirmed.PaidAt)
	require.Equal(t, sha256HexForBillingTest([]byte(raw)), billingStore.confirmed.RawPayloadSHA256)
}

func TestSyncOrderConfirmsPaidQuery(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := billingPaymentOrder{
		OrderID:            "pay_1",
		OrganizationID:     "org_1",
		WorkspaceID:        "wsp_1",
		OutTradeNo:         "agentteam_1",
		Status:             "pending",
		CreditUnit:         billingCreditUnitAgentTeamCredit,
		CreditAmount:       3000,
		Currency:           billingCurrencyCNY,
		PayableAmountCents: 3000,
		Provider:           billingProviderAlipay,
		ProviderMode:       billingProviderModePagePay,
		ExpiresAt:          now.Add(billingOrderTTL),
	}
	paidOrder := pendingOrder
	paidOrder.Status = "paid"
	paidAt := now
	paidOrder.PaidAt = &paidAt
	billingStore := &fakeBillingClient{
		order:        pendingOrder,
		confirmOrder: paidOrder,
	}
	alipayClient := &fakeAlipayClient{queryResult: alipay.QueryResult{
		Code:             alipaySuccessCode,
		OutTradeNo:       "agentteam_1",
		TradeNo:          "trade_1",
		TradeStatus:      alipayTradeStatusSuccess,
		TotalAmount:      "30.00",
		RawPayloadSHA256: "query_hash",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		BillingClient: billingStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/credit-recharge-orders/pay_1/sync", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "agentteam_1", alipayClient.queryOutTradeNo)
	require.Equal(t, "pay_1", billingStore.confirmedOrderID)
	require.Equal(t, "query", billingStore.confirmed.Source)
	require.Equal(t, "trade_1", billingStore.confirmed.ProviderTradeNo)
	require.Equal(t, alipayTradeStatusSuccess, billingStore.confirmed.ProviderTradeStatus)
	require.Equal(t, int64(3000), billingStore.confirmed.PaidAmountCents)
	require.Equal(t, now, billingStore.confirmed.PaidAt)
	require.Equal(t, "query_hash", billingStore.confirmed.RawPayloadSHA256)
	require.JSONEq(t, `{"orderId":"pay_1","outTradeNo":"agentteam_1","status":"paid","creditUnit":"agentteam_credit","creditAmount":3000,"currency":"CNY","payableAmountCents":3000,"expiresAt":"2026-06-03T10:30:00Z","paidAt":"2026-06-03T10:00:00Z"}`, recorder.Body.String())
}

func TestSyncOrderKeepsPendingWhenAlipayTradeIsNotCreatedYet(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := billingPaymentOrder{
		OrderID:            "pay_1",
		OrganizationID:     "org_1",
		WorkspaceID:        "wsp_1",
		OutTradeNo:         "agentteam_1",
		Status:             "pending",
		CreditUnit:         billingCreditUnitAgentTeamCredit,
		CreditAmount:       3000,
		Currency:           billingCurrencyCNY,
		PayableAmountCents: 3000,
		Provider:           billingProviderAlipay,
		ProviderMode:       billingProviderModePagePay,
		ExpiresAt:          now.Add(billingOrderTTL),
	}
	billingStore := &fakeBillingClient{order: pendingOrder}
	alipayClient := &fakeAlipayClient{queryResult: alipay.QueryResult{
		Code:       "40004",
		SubCode:    alipaySubCodeTradeNotExist,
		OutTradeNo: "agentteam_1",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		BillingClient: billingStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/credit-recharge-orders/pay_1/sync", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "agentteam_1", alipayClient.queryOutTradeNo)
	require.Empty(t, billingStore.confirmedOrderID)
	require.JSONEq(t, `{"orderId":"pay_1","outTradeNo":"agentteam_1","status":"pending","creditUnit":"agentteam_credit","creditAmount":3000,"currency":"CNY","payableAmountCents":3000,"expiresAt":"2026-06-03T10:30:00Z"}`, recorder.Body.String())
}

func TestSeatSummaryReturnsSeatUsageWithoutHostedBillingClient(t *testing.T) {
	t.Parallel()

	router := gin.New()
	seatStore := &fakeSeatBillingStore{
		summary: seatSummaryView{
			Entitlement: seatEntitlementView{
				OrganizationID:         "org_1",
				PlanKey:                "free",
				HumanSeatLimit:         20,
				FreeHumanSeatLimit:     20,
				OrganizationAgentLimit: 10,
				ManagedAgentsPerSeat:   10,
			},
			CurrentHumanSeats: 4,
			CurrentAgents:     2,
		},
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/seat-summary", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "org_1", seatStore.summaryOrganizationID)
	require.JSONEq(t, `{
		"organizationId":"org_1",
		"planKey":"free",
		"paid":false,
		"humanSeatLimit":20,
		"freeHumanSeatLimit":20,
		"paidHumanSeatLimit":0,
		"giftHumanSeatLimit":0,
		"currentHumanSeats":4,
		"humanSeatsOverLimit":false,
		"organizationAgentLimit":10,
		"currentAgents":2,
		"agentsOverLimit":false,
		"managedAgentsPerSeat":10
	}`, recorder.Body.String())
}

func TestCreateSeatOrderRequiresOrgAdminAndSignsPagePay(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 9, 0, 0, 0, time.UTC)
	seatStore := &fakeSeatBillingStore{
		createOrder: seatOrderView{
			ID:                "seat_order_1",
			OrganizationID:    "org_1",
			OrderType:         "new_subscription",
			Status:            "pending",
			ToPlanKey:         stringPtrForBillingTest("team"),
			BillingPeriod:     "month",
			SeatQuantityAfter: 4,
			SeatDelta:         4,
			AmountCents:       15600,
			Currency:          billingCurrencyCNY,
			Provider:          billingProviderAlipay,
			ProviderOrderID:   "seat_pay_1",
			PaymentStatus:     "pending",
			ExpiresAt:         now.Add(billingOrderTTL),
			CreatedByUserID:   "usr_1",
			CreatedAt:         now,
			UpdatedAt:         now,
		},
	}
	alipayClient := &fakeAlipayClient{pagePayURL: "https://openapi.alipay.test/seat-pay"}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_admin",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders", strings.NewReader(`{"orderType":"new_subscription","toPlanKey":"team","billingPeriod":"month","seatQuantityAfter":4}`))
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")
	request.Header.Set("Idempotency-Key", "seat-idem")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusCreated, recorder.Code)
	require.Equal(t, "org_1", seatStore.created.OrganizationID)
	require.Equal(t, "usr_1", seatStore.created.CreatedByUserID)
	require.Equal(t, "seat-idem", seatStore.created.IdempotencyKey)
	require.Equal(t, "new_subscription", seatStore.created.OrderType)
	require.Equal(t, "team", seatStore.created.ToPlanKey)
	require.Equal(t, 4, seatStore.created.SeatQuantityAfter)
	require.Equal(t, "seat_pay_1", alipayClient.pagePayInput.OutTradeNo)
	require.Equal(t, "156.00", alipayClient.pagePayInput.Amount)
	require.Equal(t, seatPaymentTimeoutExpress, alipayClient.pagePayInput.TimeoutExpress)
	require.Equal(t, "https://agentteam.test/api/v1/billing/seat-orders/alipay/notify", alipayClient.pagePayInput.NotifyURL)
	require.Contains(t, alipayClient.pagePayInput.ReturnURL, "workspace_id=wsp_1")
	require.Contains(t, alipayClient.pagePayInput.ReturnURL, "order_id=seat_order_1")
	require.JSONEq(t, `{
		"orderId":"seat_order_1",
		"orderType":"new_subscription",
		"status":"pending",
		"toPlanKey":"team",
		"billingPeriod":"month",
		"seatQuantityBefore":0,
		"seatQuantityAfter":4,
		"seatDelta":4,
		"amountCents":15600,
		"currency":"CNY",
		"provider":"alipay",
		"providerOrderId":"seat_pay_1",
		"paymentStatus":"pending",
		"paymentUrl":"https://openapi.alipay.test/seat-pay",
		"expiresAt":"2026-06-03T09:30:00Z",
		"createdAt":"2026-06-03T09:00:00Z",
		"updatedAt":"2026-06-03T09:00:00Z"
	}`, recorder.Body.String())
}

func TestSyncSeatOrderConfirmsPaidQuery(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	paidOrder := pendingOrder
	paidOrder.Status = "paid"
	paidOrder.PaymentStatus = "paid"
	paidAt := now
	paidOrder.PaidAt = &paidAt
	seatStore := &fakeSeatBillingStore{
		order:       pendingOrder,
		orderFound:  true,
		settleOrder: paidOrder,
	}
	alipayClient := &fakeAlipayClient{queryResult: alipay.QueryResult{
		Code:             alipaySuccessCode,
		OutTradeNo:       "seat_pay_1",
		TradeNo:          "trade_1",
		TradeStatus:      alipayTradeStatusSuccess,
		TotalAmount:      "156.00",
		RawPayloadSHA256: "query_hash",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/seat_order_1/sync", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "seat_pay_1", alipayClient.queryOutTradeNo)
	require.Equal(t, billingProviderAlipay, seatStore.settled.Provider)
	require.Equal(t, "seat_pay_1", seatStore.settled.ProviderOrderID)
	require.Equal(t, int64(15600), seatStore.settled.AmountCents)
	require.Equal(t, billingCurrencyCNY, seatStore.settled.Currency)
	require.NotNil(t, seatStore.settled.PaidAt)
	require.Equal(t, now, *seatStore.settled.PaidAt)
	require.Contains(t, seatStore.settled.RawPayloadJSON, `"raw_payload_sha256":"query_hash"`)
	require.JSONEq(t, `{
		"orderId":"seat_order_1",
		"orderType":"new_subscription",
		"status":"paid",
		"toPlanKey":"team",
		"billingPeriod":"month",
		"seatQuantityBefore":0,
		"seatQuantityAfter":4,
		"seatDelta":4,
		"amountCents":15600,
		"currency":"CNY",
		"provider":"alipay",
		"providerOrderId":"seat_pay_1",
		"paymentStatus":"paid",
		"expiresAt":"2026-06-03T10:30:00Z",
		"paidAt":"2026-06-03T10:00:00Z",
		"createdAt":"2026-06-03T10:00:00Z",
		"updatedAt":"2026-06-03T10:00:00Z"
	}`, recorder.Body.String())
}

func TestSyncSeatOrderKeepsPendingWhenAlipayTradeIsNotCreatedYet(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	seatStore := &fakeSeatBillingStore{
		order:      pendingOrder,
		orderFound: true,
	}
	alipayClient := &fakeAlipayClient{queryResult: alipay.QueryResult{
		Code:       "40004",
		SubCode:    alipaySubCodeTradeNotExist,
		OutTradeNo: "seat_pay_1",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/seat_order_1/sync", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "seat_pay_1", alipayClient.queryOutTradeNo)
	require.Empty(t, seatStore.settled.ProviderOrderID)
	require.JSONEq(t, `{
		"orderId":"seat_order_1",
		"orderType":"new_subscription",
		"status":"pending",
		"toPlanKey":"team",
		"billingPeriod":"month",
		"seatQuantityBefore":0,
		"seatQuantityAfter":4,
		"seatDelta":4,
		"amountCents":15600,
		"currency":"CNY",
		"provider":"alipay",
		"providerOrderId":"seat_pay_1",
		"paymentStatus":"pending",
		"expiresAt":"2026-06-03T10:30:00Z",
		"createdAt":"2026-06-03T10:00:00Z",
		"updatedAt":"2026-06-03T10:00:00Z"
	}`, recorder.Body.String())
}

func TestSyncSeatOrderRetriesPaidUnappliedOrder(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	unappliedOrder := fakePendingSeatOrder(now)
	unappliedOrder.Status = "paid_unapplied"
	unappliedOrder.PaymentStatus = "paid"
	paidOrder := unappliedOrder
	paidOrder.Status = "paid"
	paidAt := now
	paidOrder.PaidAt = &paidAt
	seatStore := &fakeSeatBillingStore{
		order:       unappliedOrder,
		orderFound:  true,
		settleOrder: paidOrder,
	}
	alipayClient := &fakeAlipayClient{queryResult: alipay.QueryResult{
		Code:             alipaySuccessCode,
		OutTradeNo:       "seat_pay_1",
		TradeNo:          "trade_1",
		TradeStatus:      alipayTradeStatusSuccess,
		TotalAmount:      "156.00",
		RawPayloadSHA256: "query_hash",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/seat_order_1/sync", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "seat_pay_1", alipayClient.queryOutTradeNo)
	require.Equal(t, billingProviderAlipay, seatStore.settled.Provider)
	require.Equal(t, "seat_pay_1", seatStore.settled.ProviderOrderID)
	require.Equal(t, int64(15600), seatStore.settled.AmountCents)
	require.JSONEq(t, `{
		"orderId":"seat_order_1",
		"orderType":"new_subscription",
		"status":"paid",
		"toPlanKey":"team",
		"billingPeriod":"month",
		"seatQuantityBefore":0,
		"seatQuantityAfter":4,
		"seatDelta":4,
		"amountCents":15600,
		"currency":"CNY",
		"provider":"alipay",
		"providerOrderId":"seat_pay_1",
		"paymentStatus":"paid",
		"expiresAt":"2026-06-03T10:30:00Z",
		"paidAt":"2026-06-03T10:00:00Z",
		"createdAt":"2026-06-03T10:00:00Z",
		"updatedAt":"2026-06-03T10:00:00Z"
	}`, recorder.Body.String())
}

func TestCancelSeatOrderClosesAlipayTradeBeforeLocalCancel(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	cancelledOrder := pendingOrder
	cancelledOrder.Status = "cancelled"
	cancelledOrder.PaymentStatus = "closed"
	seatStore := &fakeSeatBillingStore{
		order:       pendingOrder,
		orderFound:  true,
		cancelOrder: cancelledOrder,
	}
	alipayClient := &fakeAlipayClient{closeResult: alipay.CloseResult{
		Code:       alipaySuccessCode,
		OutTradeNo: "seat_pay_1",
		TradeNo:    "trade_1",
	}}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/seat_order_1/cancel", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "seat_pay_1", alipayClient.closeOutTradeNo)
	require.Equal(t, "org_1", seatStore.cancelled.OrganizationID)
	require.Equal(t, "seat_order_1", seatStore.cancelled.OrderID)
	require.Equal(t, "usr_1", seatStore.cancelled.CancelledByUserID)
	require.Contains(t, seatStore.cancelled.RawPayloadJSON, `"provider_trade_no":"trade_1"`)
}

func TestCancelSeatOrderReconcilesAlreadyPaidProviderTrade(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	paidOrder := pendingOrder
	paidOrder.Status = "paid"
	paidOrder.PaymentStatus = "paid"
	paidOrder.PaidAt = &now
	seatStore := &fakeSeatBillingStore{
		order:       pendingOrder,
		orderFound:  true,
		settleOrder: paidOrder,
	}
	alipayClient := &fakeAlipayClient{
		closeResult: alipay.CloseResult{
			Code:       "40004",
			OutTradeNo: "seat_pay_1",
		},
		queryResult: alipay.QueryResult{
			Code:             alipaySuccessCode,
			OutTradeNo:       "seat_pay_1",
			TradeNo:          "trade_1",
			TradeStatus:      alipayTradeStatusSuccess,
			TotalAmount:      "156.00",
			RawPayloadSHA256: "query_hash",
		},
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		SeatBilling:   seatStore,
		Alipay:        alipayClient,
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/seat_order_1/cancel", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "seat_pay_1", alipayClient.closeOutTradeNo)
	require.Equal(t, "seat_pay_1", alipayClient.queryOutTradeNo)
	require.Equal(t, billingProviderAlipay, seatStore.settled.Provider)
	require.Equal(t, "seat_pay_1", seatStore.settled.ProviderOrderID)
	require.Equal(t, int64(15600), seatStore.settled.AmountCents)
	require.Empty(t, seatStore.cancelled.OrderID)
	require.JSONEq(t, `{
		"orderId":"seat_order_1",
		"orderType":"new_subscription",
		"status":"paid",
		"toPlanKey":"team",
		"billingPeriod":"month",
		"seatQuantityBefore":0,
		"seatQuantityAfter":4,
		"seatDelta":4,
		"amountCents":15600,
		"currency":"CNY",
		"provider":"alipay",
		"providerOrderId":"seat_pay_1",
		"paymentStatus":"paid",
		"expiresAt":"2026-06-03T10:30:00Z",
		"paidAt":"2026-06-03T10:00:00Z",
		"createdAt":"2026-06-03T10:00:00Z",
		"updatedAt":"2026-06-03T10:00:00Z"
	}`, recorder.Body.String())
}

func TestSeatAlipayNotifySettlesPaidOrder(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	seatStore := &fakeSeatBillingStore{
		orderByProvider:      pendingOrder,
		orderByProviderFound: true,
		settleOrder:          pendingOrder,
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth:   fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{},
		SeatBilling:   seatStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})
	form := url.Values{
		"out_trade_no": {"seat_pay_1"},
		"trade_no":     {"trade_1"},
		"trade_status": {alipayTradeStatusSuccess},
		"total_amount": {"156.00"},
		"subject":      {seatPaymentSubject},
		"gmt_payment":  {"2026-06-03 18:00:00"},
		"notify_time":  {"2026-06-03 18:00:01"},
		"sign":         {"signature"},
		"sign_type":    {"RSA2"},
	}
	raw := form.Encode()

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/alipay/notify", strings.NewReader(raw))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "success", recorder.Body.String())
	require.Equal(t, "seat_pay_1", seatStore.providerOrderID)
	require.Equal(t, int64(15600), seatStore.settled.AmountCents)
	require.NotNil(t, seatStore.settled.PaidAt)
	require.Equal(t, now, *seatStore.settled.PaidAt)
	require.Contains(t, seatStore.settled.RawPayloadJSON, sha256HexForBillingTest([]byte(raw)))
}

func TestSeatAlipayNotifyRejectsWrongSubject(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 3, 10, 0, 0, 0, time.UTC)
	pendingOrder := fakePendingSeatOrder(now)
	seatStore := &fakeSeatBillingStore{
		orderByProvider:      pendingOrder,
		orderByProviderFound: true,
		settleOrder:          pendingOrder,
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth:   fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{},
		SeatBilling:   seatStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
	})
	form := url.Values{
		"out_trade_no": {"seat_pay_1"},
		"trade_no":     {"trade_1"},
		"trade_status": {alipayTradeStatusSuccess},
		"total_amount": {"156.00"},
		"subject":      {"AgentTeam hosted credits"},
		"gmt_payment":  {"2026-06-03 18:00:00"},
		"notify_time":  {"2026-06-03 18:00:01"},
		"sign":         {"signature"},
		"sign_type":    {"RSA2"},
	}

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/billing/seat-orders/alipay/notify", strings.NewReader(form.Encode()))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "fail", recorder.Body.String())
	require.Empty(t, seatStore.providerOrderID)
	require.Zero(t, seatStore.settled.AmountCents)
}

func TestBillingHistoryMergesHostedCreditAndSeatRows(t *testing.T) {
	t.Parallel()

	router := gin.New()
	now := time.Date(2026, 6, 9, 10, 0, 0, 0, time.UTC)
	hostedPaidAt := time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)
	seatPaidAt := time.Date(2026, 6, 9, 9, 0, 0, 0, time.UTC)
	billingStore := &fakeBillingClient{
		paymentsPage: billingPaymentOrderPage{Items: []billingPaymentOrder{{
			OrderID:            "hosted_pay_1",
			OutTradeNo:         "hosted_out_1",
			Status:             "paid",
			Currency:           billingCurrencyCNY,
			PayableAmountCents: 3000,
			CreatedAt:          hostedPaidAt.Add(-time.Minute),
			PaidAt:             &hostedPaidAt,
		}}},
	}
	seatStore := &fakeSeatBillingStore{
		history: []seatPaymentHistoryView{{
			OrderID:         "seat_order_1",
			ProviderOrderID: "seat_pay_1",
			Status:          "paid",
			AmountCents:     15600,
			Currency:        billingCurrencyCNY,
			CreatedAt:       seatPaidAt.Add(-time.Minute),
			PaidAt:          &seatPaidAt,
		}},
	}
	RegisterRoutes(router, Dependencies{
		SessionAuth: fakeSessionAuth{userID: "usr_1"},
		Organizations: fakeOrganizationProvider{summary: organization.OrganizationSummary{
			ID:   "org_1",
			Role: "org_owner",
		}},
		BillingClient: billingStore,
		SeatBilling:   seatStore,
		Alipay:        &fakeAlipayClient{},
		Enabled:       true,
		PublicBaseURL: "https://agentteam.test",
		Now:           func() time.Time { return now },
	})

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/billing/history?month=2026-06&product=all&limit=2", nil)
	request.Header.Set("Authorization", "Bearer token")
	request.Header.Set("X-Workspace-Id", "wsp_1")

	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "2026-06", billingStore.paymentsOptions.Month)
	require.Equal(t, 2, billingStore.paymentsOptions.Limit)
	require.Equal(t, time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), seatStore.historyOptions.MonthStart)
	require.JSONEq(t, `{"items":[
		{"productType":"seat","sourceOrderId":"seat_order_1","providerPaymentId":"seat_pay_1","status":"paid","amountCents":15600,"currency":"CNY","createdAt":"2026-06-09T08:59:00Z","paidAt":"2026-06-09T09:00:00Z"},
		{"productType":"hosted_credit","sourceOrderId":"hosted_pay_1","providerPaymentId":"hosted_out_1","status":"paid","amountCents":3000,"currency":"CNY","createdAt":"2026-06-08T09:59:00Z","paidAt":"2026-06-08T10:00:00Z"}
	]}`, recorder.Body.String())
}

func TestParseCNYAmountCents(t *testing.T) {
	t.Parallel()

	for _, tc := range []struct {
		raw      string
		expected int64
	}{
		{raw: "30", expected: 3000},
		{raw: "30.0", expected: 3000},
		{raw: "30.05", expected: 3005},
		{raw: "0.01", expected: 1},
	} {
		actual, err := parseCNYAmountCents(tc.raw)
		require.NoError(t, err)
		require.Equal(t, tc.expected, actual)
	}

	_, err := parseCNYAmountCents("30.001")
	require.Error(t, err)
}

type fakeSessionAuth struct {
	userID string
}

func (fake fakeSessionAuth) AuthenticateSession(context.Context, string) (identity.AuthenticatedUser, error) {
	return identity.AuthenticatedUser{User: model.User{ID: fake.userID}}, nil
}

type fakeOrganizationProvider struct {
	summary organization.OrganizationSummary
	err     error
}

func (fake fakeOrganizationProvider) GetWorkspaceOrganizationForUser(context.Context, string, string) (organization.OrganizationSummary, error) {
	if fake.err != nil {
		return organization.OrganizationSummary{}, fake.err
	}
	return fake.summary, nil
}

type fakeAlipayClient struct {
	pagePayInput    alipay.PagePayInput
	pagePayURL      string
	queryOutTradeNo string
	queryResult     alipay.QueryResult
	queryErr        error
	closeOutTradeNo string
	closeResult     alipay.CloseResult
	closeErr        error
	verifyErr       error
}

func (fake *fakeAlipayClient) CreatePagePayURL(input alipay.PagePayInput) (string, error) {
	fake.pagePayInput = input
	return fake.pagePayURL, nil
}

func (fake *fakeAlipayClient) VerifyNotify(url.Values) error {
	return fake.verifyErr
}

func (fake *fakeAlipayClient) QueryTrade(_ context.Context, outTradeNo string) (alipay.QueryResult, error) {
	fake.queryOutTradeNo = outTradeNo
	return fake.queryResult, fake.queryErr
}

func (fake *fakeAlipayClient) CloseTrade(_ context.Context, outTradeNo string) (alipay.CloseResult, error) {
	fake.closeOutTradeNo = outTradeNo
	return fake.closeResult, fake.closeErr
}

type fakeBillingClient struct {
	summaryCalled         bool
	pricing               billingPricing
	pricingOrganizationID string
	paymentsPage          billingPaymentOrderPage
	paymentsOptions       billingListOptions
	created               createCreditRechargeOrderParams
	createOrder           billingPaymentOrder
	order                 billingPaymentOrder
	orderByOutTradeNo     billingPaymentOrder
	confirmOrder          billingPaymentOrder
	confirmedOrderID      string
	confirmed             confirmCreditRechargeOrderPaidParams
}

func (fake *fakeBillingClient) LoadSummary(context.Context, string) (billingSummary, error) {
	fake.summaryCalled = true
	return billingSummary{}, nil
}

func (fake *fakeBillingClient) ListLedger(context.Context, string, billingListOptions) (billingLedgerPage, error) {
	return billingLedgerPage{}, nil
}

func (fake *fakeBillingClient) ListModelUsage(context.Context, string, billingListOptions) (billingModelUsage, error) {
	return billingModelUsage{}, nil
}

func (fake *fakeBillingClient) ListPayments(_ context.Context, _ string, options billingListOptions) (billingPaymentOrderPage, error) {
	fake.paymentsOptions = options
	if len(fake.paymentsPage.Items) > 0 {
		return fake.paymentsPage, nil
	}
	return billingPaymentOrderPage{}, nil
}

func (fake *fakeBillingClient) LoadPricing(_ context.Context, organizationID string) (billingPricing, error) {
	fake.pricingOrganizationID = organizationID
	return fake.pricing, nil
}

func (fake *fakeBillingClient) CreateCreditRechargeOrder(_ context.Context, params createCreditRechargeOrderParams) (billingPaymentOrder, error) {
	fake.created = params
	return fake.createOrder, nil
}

func (fake *fakeBillingClient) LoadCreditRechargeOrder(context.Context, string, string) (billingPaymentOrder, error) {
	return fake.order, nil
}

func (fake *fakeBillingClient) LoadCreditRechargeOrderByOutTradeNo(context.Context, string) (billingPaymentOrder, error) {
	return fake.orderByOutTradeNo, nil
}

func (fake *fakeBillingClient) ConfirmCreditRechargeOrderPaid(_ context.Context, orderID string, params confirmCreditRechargeOrderPaidParams) (billingPaymentOrder, error) {
	fake.confirmedOrderID = orderID
	fake.confirmed = params
	return fake.confirmOrder, nil
}

func (fake *fakeBillingClient) CloseCreditRechargeOrder(context.Context, string, closeCreditRechargeOrderParams) (billingPaymentOrder, error) {
	return billingPaymentOrder{}, nil
}

type fakeSeatBillingStore struct {
	plans                 []seatPlanView
	summary               seatSummaryView
	summaryOrganizationID string
	subscription          seatSubscriptionView
	subscriptionFound     bool
	created               seatOrderCreateParams
	createOrder           seatOrderView
	order                 seatOrderView
	orderFound            bool
	provider              string
	providerOrderID       string
	orderByProvider       seatOrderView
	orderByProviderFound  bool
	listOptions           seatOrderListOptions
	listOrders            []seatOrderView
	settled               seatPaymentSettleParams
	settleOrder           seatOrderView
	cancelled             seatOrderCancelParams
	cancelOrder           seatOrderView
	historyOptions        seatPaymentHistoryOptions
	history               []seatPaymentHistoryView
}

func (fake *fakeSeatBillingStore) ListSeatPlans(context.Context) ([]seatPlanView, error) {
	return fake.plans, nil
}

func (fake *fakeSeatBillingStore) LoadSeatSummary(_ context.Context, organizationID string, _ time.Time) (seatSummaryView, error) {
	fake.summaryOrganizationID = organizationID
	return fake.summary, nil
}

func (fake *fakeSeatBillingStore) LoadSeatSubscription(context.Context, string) (seatSubscriptionView, bool, error) {
	return fake.subscription, fake.subscriptionFound, nil
}

func (fake *fakeSeatBillingStore) CreateSeatOrder(_ context.Context, params seatOrderCreateParams) (seatOrderView, error) {
	fake.created = params
	return fake.createOrder, nil
}

func (fake *fakeSeatBillingStore) LoadSeatOrder(_ context.Context, _ string, _ string) (seatOrderView, bool, error) {
	return fake.order, fake.orderFound, nil
}

func (fake *fakeSeatBillingStore) LoadSeatOrderByProviderOrderID(_ context.Context, provider string, providerOrderID string) (seatOrderView, bool, error) {
	fake.provider = provider
	fake.providerOrderID = providerOrderID
	return fake.orderByProvider, fake.orderByProviderFound, nil
}

func (fake *fakeSeatBillingStore) ListSeatOrders(_ context.Context, params seatOrderListOptions) ([]seatOrderView, error) {
	fake.listOptions = params
	return fake.listOrders, nil
}

func (fake *fakeSeatBillingStore) SettleSeatPaymentPaid(_ context.Context, params seatPaymentSettleParams) (seatOrderView, error) {
	fake.settled = params
	return fake.settleOrder, nil
}

func (fake *fakeSeatBillingStore) CancelSeatOrder(_ context.Context, params seatOrderCancelParams) (seatOrderView, error) {
	fake.cancelled = params
	return fake.cancelOrder, nil
}

func (fake *fakeSeatBillingStore) ListSeatPaymentHistory(_ context.Context, params seatPaymentHistoryOptions) ([]seatPaymentHistoryView, error) {
	fake.historyOptions = params
	return fake.history, nil
}

func fakePendingSeatOrder(now time.Time) seatOrderView {
	return seatOrderView{
		ID:                "seat_order_1",
		OrganizationID:    "org_1",
		OrderType:         "new_subscription",
		Status:            "pending",
		ToPlanKey:         stringPtrForBillingTest("team"),
		BillingPeriod:     "month",
		SeatQuantityAfter: 4,
		SeatDelta:         4,
		AmountCents:       15600,
		Currency:          billingCurrencyCNY,
		Provider:          billingProviderAlipay,
		ProviderOrderID:   "seat_pay_1",
		PaymentStatus:     "pending",
		ExpiresAt:         now.Add(billingOrderTTL),
		CreatedByUserID:   "usr_1",
		CreatedAt:         now,
		UpdatedAt:         now,
	}
}

func stringPtrForBillingTest(value string) *string {
	return &value
}

func sha256HexForBillingTest(raw []byte) string {
	sum := sha256.Sum256(raw)
	return fmt.Sprintf("%x", sum[:])
}
