package tests

import (
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/promotion"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/suite"
)

type PromotionTestSuite struct {
	tests.BillingTestSuite
}

func TestPromotionSuite(t *testing.T) {
	suite.Run(t, new(PromotionTestSuite))
}

var referralModel = &marketing.ReferralModel{}

func (s *PromotionTestSuite) SetupTest() {
	s.BillingTestSuite.SetupTest()
	assert := s.Assert()

	// Setup tenant 1 (referrer A)
	err := datainit.CreatePaymentMock(1)
	assert.NoError(err)
	_, err = datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)
	assert.NoError(err)

	// Setup tenant 100 (referee B) — only needs billing config, no payment mock
	// (CreatePaymentMock would call CreateCompany again, causing duplicate key error)
	_, err = datainit.CreateBillingConfig(100, entity.BillingConfigType_DirectCustomer)
	assert.NoError(err)

	// Set referral config (global) + per-type amounts
	config.Get().Set(config.ReferralRewardConfig, `{"credits_validity_days":365,"usage":"referral"}`)
	config.Get().Set(config.ReferralAmountSignup, 5)
	config.Get().Set(config.ReferralAmountRefereeSignup, 5)
	config.Get().Set(config.ReferralAmountBindCard, 5)
	config.Get().Set(config.ReferralAmountRefereeBindCard, 5)
	config.Get().Set(config.ReferralAmountFirstUse, 3)
	config.Get().Set(config.ReferralAmountContinuousUse, 10)
	config.Get().Set(config.ReferralAmountRefereeContinuousUse, 10)
	config.Get().Set(config.ReferralShareLinkBase, "https://tidbcloud.com/signup")
	config.Get().Set(config.CreditsPricePackage, datainit.NoUsePackageId)
	config.Get().Set(config.PromotionFirstUseThreshold, 10)
	config.Get().Set(config.PromotionContinuousUseDays, 7)
	config.Get().Set(config.PromotionContinuousUseDaily, 10)

	timeutils.GetNowFunc = func() time.Time {
		return datainit.BillingDate
	}
}

// helper to create a campaign and referral code for tenant 1
func (s *PromotionTestSuite) setupCampaignAndCode() string {
	assert := s.Assert()
	_, err := referralModel.CreateCampaign(&forms.CreateCampaignReq{
		Name:              "Test Campaign",
		TotalBudget:       decimal.NewFromInt(10000),
		IsWebsiteCampaign: false,
	}, "admin@test.com")
	assert.NoError(err)

	codeResp, err := referralModel.GetOrCreateReferralCode(1, false)
	assert.NoError(err)
	return codeResp.Code
}

// helper: simulate full signup flow (referrer + new user rewards)
func (s *PromotionTestSuite) doSignup(code string, tenantID uint) {
	assert := s.Assert()
	err := promotion.RewardReferralSignup(code, tenantID)
	assert.NoError(err)
	err = promotion.RewardNewUserSignup(code, tenantID)
	assert.NoError(err)
}

// helper: simulate full bindcard flow (referrer + new user rewards)
func (s *PromotionTestSuite) doBindCard(tenantID uint) {
	assert := s.Assert()
	err := promotion.RewardReferralBindCard(tenantID)
	assert.NoError(err)
	err = promotion.RewardNewUserBindCard(tenantID)
	assert.NoError(err)
}

// ========== RewardReferralSignup Tests ==========

func (s *PromotionTestSuite) TestRewardReferralSignupEmpty() {
	// No referral code should be a no-op
	err := promotion.RewardReferralSignup("", 100)
	s.Assert().NoError(err)
}

func (s *PromotionTestSuite) TestRewardReferralSignupReferrerOnly() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// Only referrer reward
	err := promotion.RewardReferralSignup(code, 100)
	assert.NoError(err)

	// Verify referrer (A, tenant 1) got signup reward
	var signupEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_Signup).First(&signupEvent).Error
	assert.NoError(err)
	assert.True(signupEvent.Rewarded)
	assert.Equal(uint(1), signupEvent.ReferrerTenantID)

	// No referee_signup event yet
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_RefereeSignup).Count(&count)
	assert.Equal(int64(0), count)

	// Budget: only $5 (referrer only)
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(5), list.UsedBudgetAll)
}

// ========== RewardNewUserSignup Tests ==========

func (s *PromotionTestSuite) TestRewardNewUserSignupEmpty() {
	err := promotion.RewardNewUserSignup("", 100)
	s.Assert().NoError(err)
}

func (s *PromotionTestSuite) TestRewardNewUserSignupRefereeOnly() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// Only referee reward
	err := promotion.RewardNewUserSignup(code, 100)
	assert.NoError(err)

	// Verify referee (B, tenant 100) got referee_signup reward
	var refereeEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_RefereeSignup).First(&refereeEvent).Error
	assert.NoError(err)
	assert.True(refereeEvent.Rewarded)

	// No signup event for referrer
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_Signup).Count(&count)
	assert.Equal(int64(0), count)
}

func (s *PromotionTestSuite) TestSignupBothParties() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	s.doSignup(code, 100)

	// Verify both events exist
	var signupEvent entity.ReferralEvent
	err := db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_Signup).First(&signupEvent).Error
	assert.NoError(err)
	assert.True(signupEvent.Rewarded)

	var refereeEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_RefereeSignup).First(&refereeEvent).Error
	assert.NoError(err)
	assert.True(refereeEvent.Rewarded)

	// Budget: 2 x $5 = $10
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(10), list.UsedBudgetAll)
}

func (s *PromotionTestSuite) TestSignupIdempotent() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	s.doSignup(code, 100)

	// Call again — should not create duplicate events
	s.doSignup(code, 100)

	// Should still be 2 events total (signup + referee_signup), not 4
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ?", 100).Count(&count)
	assert.Equal(int64(2), count)

	// Budget should still be $10
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(10), list.UsedBudgetAll)
}

func (s *PromotionTestSuite) TestSignupBudgetExhausted() {
	assert := s.Assert()

	// Create campaign with only $7 (enough for one $5 reward, not two)
	_, err := referralModel.CreateCampaign(&forms.CreateCampaignReq{
		Name:              "Small Budget",
		TotalBudget:       decimal.NewFromInt(7),
		IsWebsiteCampaign: false,
	}, "admin@test.com")
	assert.NoError(err)

	codeResp, err := referralModel.GetOrCreateReferralCode(1, false)
	assert.NoError(err)

	s.doSignup(codeResp.Code, 100)

	// Referrer should get signup reward ($5)
	var signupEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_Signup).First(&signupEvent).Error
	assert.NoError(err)
	assert.True(signupEvent.Rewarded)

	// Referee should have event recorded but budget exhausted (only $2 remaining < $5)
	var refereeEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_RefereeSignup).First(&refereeEvent).Error
	assert.NoError(err)
	assert.False(refereeEvent.Rewarded)
	assert.Equal(entity.ReferralSkipReason_BudgetExhausted, refereeEvent.SkipReason)
}

// ========== RewardReferralBindCard Tests ==========

func (s *PromotionTestSuite) TestRewardReferralBindCardNotReferred() {
	// Tenant 200 was never referred — should be a no-op
	err := promotion.RewardReferralBindCard(200)
	s.Assert().NoError(err)
}

func (s *PromotionTestSuite) TestRewardNewUserBindCardNotReferred() {
	err := promotion.RewardNewUserBindCard(200)
	s.Assert().NoError(err)
}

func (s *PromotionTestSuite) TestBindCardBothParties() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// First signup
	s.doSignup(code, 100)

	// Now bindcard
	s.doBindCard(100)

	// Verify referrer (A) got bindcard reward
	var bindcardEvent entity.ReferralEvent
	err := db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_BindCard).First(&bindcardEvent).Error
	assert.NoError(err)
	assert.True(bindcardEvent.Rewarded)

	// Verify referee (B) got referee_bindcard reward
	var refereeBindcardEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_RefereeBindCard).First(&refereeBindcardEvent).Error
	assert.NoError(err)
	assert.True(refereeBindcardEvent.Rewarded)

	// Total events: signup + referee_signup + bindcard + referee_bindcard = 4
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ?", 100).Count(&count)
	assert.Equal(int64(4), count)

	// Budget: 4 x $5 = $20
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(20), list.UsedBudgetAll)
}

func (s *PromotionTestSuite) TestBindCardIdempotent() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	s.doSignup(code, 100)
	s.doBindCard(100)

	// Call again — no new events
	s.doBindCard(100)

	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ?", 100).Count(&count)
	assert.Equal(int64(4), count) // signup + referee_signup + bindcard + referee_bindcard
}

// ========== RewardFirstUse Tests ==========

// mockClinicHandler is a test double for the clinic API
type mockClinicHandler struct {
	clusterIDs map[uint][]string
	requests   map[string]float64
}

func (m *mockClinicHandler) GetTenantClusterIDs(tenantID uint) ([]string, error) {
	if ids, ok := m.clusterIDs[tenantID]; ok {
		return ids, nil
	}
	return nil, nil
}

func (m *mockClinicHandler) GetClusterRequests(clusterID string, start, end time.Time) (float64, error) {
	if reqs, ok := m.requests[clusterID]; ok {
		return reqs, nil
	}
	return 0, nil
}

func (s *PromotionTestSuite) TestRewardFirstUseNoCandidates() {
	// No signup events exist, RewardFirstUse should be a no-op
	promotion.RewardFirstUse()

	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("event_type = ?", entity.ReferralEventType_FirstUse).Count(&count)
	s.Assert().Equal(int64(0), count)
}

func (s *PromotionTestSuite) TestRewardFirstUseQualifies() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// Create signup event for tenant 100 (referrer reward creates the signup event)
	err := promotion.RewardReferralSignup(code, 100)
	assert.NoError(err)

	// Mock clinic: tenant 100 has one cluster with enough requests
	mock := &mockClinicHandler{
		clusterIDs: map[uint][]string{100: {"cluster-1"}},
		requests:   map[string]float64{"cluster-1": 100},
	}
	marketing.SetClinicHandler(mock)
	defer marketing.SetClinicHandler(nil)

	promotion.RewardFirstUse()

	// Verify first_use event was created
	var event entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_FirstUse).First(&event).Error
	assert.NoError(err)
	assert.True(event.Rewarded)
}

func (s *PromotionTestSuite) TestRewardFirstUseBelowThreshold() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	err := promotion.RewardReferralSignup(code, 100)
	assert.NoError(err)

	// Mock clinic: tenant 100 has requests below threshold (10)
	mock := &mockClinicHandler{
		clusterIDs: map[uint][]string{100: {"cluster-1"}},
		requests:   map[string]float64{"cluster-1": 5},
	}
	marketing.SetClinicHandler(mock)
	defer marketing.SetClinicHandler(nil)

	promotion.RewardFirstUse()

	// No first_use event should be created
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_FirstUse).Count(&count)
	assert.Equal(int64(0), count)
}

// ========== RewardContinuousUse Tests ==========

func (s *PromotionTestSuite) TestRewardContinuousUseNoCandidates() {
	promotion.RewardContinuousUse()

	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("event_type = ?", entity.ReferralEventType_ContinuousUse).Count(&count)
	s.Assert().Equal(int64(0), count)
}

func (s *PromotionTestSuite) TestRewardContinuousUseQualifies() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// Create signup event for tenant 100
	err := promotion.RewardReferralSignup(code, 100)
	assert.NoError(err)

	mock := &mockClinicHandler{
		clusterIDs: map[uint][]string{100: {"cluster-1"}},
		requests:   map[string]float64{"cluster-1": 100},
	}
	marketing.SetClinicHandler(mock)
	defer marketing.SetClinicHandler(nil)

	// Trigger first_use first
	promotion.RewardFirstUse()

	// Verify first_use was granted
	var firstUseEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_FirstUse).First(&firstUseEvent).Error
	assert.NoError(err)

	// Now trigger continuous_use (mock returns enough daily requests)
	promotion.RewardContinuousUse()

	// Verify continuous_use event was created
	var continuousEvent entity.ReferralEvent
	err = db.GetDB().Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_ContinuousUse).First(&continuousEvent).Error
	assert.NoError(err)
	assert.True(continuousEvent.Rewarded)
}

func (s *PromotionTestSuite) TestRewardContinuousUseBelowThreshold() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	err := promotion.RewardReferralSignup(code, 100)
	assert.NoError(err)

	// First, grant first_use with high enough requests
	highMock := &mockClinicHandler{
		clusterIDs: map[uint][]string{100: {"cluster-1"}},
		requests:   map[string]float64{"cluster-1": 100},
	}
	marketing.SetClinicHandler(highMock)
	promotion.RewardFirstUse()

	// Now set low requests for continuous_use check
	lowMock := &mockClinicHandler{
		clusterIDs: map[uint][]string{100: {"cluster-1"}},
		requests:   map[string]float64{"cluster-1": 3},
	}
	marketing.SetClinicHandler(lowMock)
	defer marketing.SetClinicHandler(nil)

	promotion.RewardContinuousUse()

	// No continuous_use event should exist
	var count int64
	db.GetDB().Model(&entity.ReferralEvent{}).Where("referee_tenant_id = ? AND event_type = ?", 100, entity.ReferralEventType_ContinuousUse).Count(&count)
	assert.Equal(int64(0), count)
}

// ========== RewardCommunityPayment Tests ==========

func (s *PromotionTestSuite) TestRewardCommunityPaymentReferred() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()
	config.Get().Set(config.CommunityRewardRate, 0.03)

	// Signup first so tenant 100 is a referred user
	s.doSignup(code, 100)

	// Budget used so far: 2 x $5 = $10
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(10), list.UsedBudgetAll)

	// Simulate invoice-paid event with $100 payment
	promotion.RewardCommunityPayment(&forms.InvoicePaidEvent{
		TenantID:   100,
		PaidAmount: decimal.NewFromInt(100),
	})

	// 3% of $100 = $3 community reward
	list, err = referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(13), list.UsedBudgetAll) // $10 + $3

	// Verify credits package was created for tenant 100
	var count int64
	db.GetDB().Model(&entity.CreditsPackage{}).
		Where("tenant_id = ? AND usage_extend = ?", 100, entity.ReferralEventType_CommunityPayment).
		Count(&count)
	assert.Equal(int64(1), count)
}

func (s *PromotionTestSuite) TestRewardCommunityPaymentMultiple() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()
	config.Get().Set(config.CommunityRewardRate, 0.03)

	s.doSignup(code, 100)

	// Two payments — both should create rewards
	promotion.RewardCommunityPayment(&forms.InvoicePaidEvent{
		TenantID:   100,
		PaidAmount: decimal.NewFromInt(100),
	})
	promotion.RewardCommunityPayment(&forms.InvoicePaidEvent{
		TenantID:   100,
		PaidAmount: decimal.NewFromInt(200),
	})

	// $10 (signup) + $3 (3% of $100) + $6 (3% of $200) = $19
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(19), list.UsedBudgetAll)

	// 2 credits packages
	var count int64
	db.GetDB().Model(&entity.CreditsPackage{}).
		Where("tenant_id = ? AND usage_extend = ?", 100, entity.ReferralEventType_CommunityPayment).
		Count(&count)
	assert.Equal(int64(2), count)
}

func (s *PromotionTestSuite) TestRewardCommunityPaymentNotReferred() {
	assert := s.Assert()
	s.setupCampaignAndCode()
	config.Get().Set(config.CommunityRewardRate, 0.03)

	// Tenant 200 was never referred — no signup event
	promotion.RewardCommunityPayment(&forms.InvoicePaidEvent{
		TenantID:   200,
		PaidAmount: decimal.NewFromInt(100),
	})

	// No credits should be created
	var count int64
	db.GetDB().Model(&entity.CreditsPackage{}).
		Where("tenant_id = ? AND usage_extend = ?", 200, entity.ReferralEventType_CommunityPayment).
		Count(&count)
	assert.Equal(int64(0), count)

	// Budget unchanged
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.True(list.UsedBudgetAll.Equal(decimal.Zero))
}

func (s *PromotionTestSuite) TestRewardCommunityPaymentZeroPaid() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()
	config.Get().Set(config.CommunityRewardRate, 0.03)

	s.doSignup(code, 100)
	budgetBefore, _ := referralModel.ListCampaigns()

	// Zero payment — no reward
	promotion.RewardCommunityPayment(&forms.InvoicePaidEvent{
		TenantID:   100,
		PaidAmount: decimal.Zero,
	})

	budgetAfter, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(budgetBefore.UsedBudgetAll, budgetAfter.UsedBudgetAll)
}

// ========== Full Promotion Flow Test ==========

func (s *PromotionTestSuite) TestFullPromotionFlow() {
	assert := s.Assert()
	code := s.setupCampaignAndCode()

	// 1. B registers with A's referral code — A and B get signup rewards separately
	s.doSignup(code, 100)

	// 2. B binds credit card — A and B get bindcard rewards separately
	s.doBindCard(100)

	// 3. Verify all 4 events exist
	var events []entity.ReferralEvent
	err := db.GetDB().Where("referee_tenant_id = ?", 100).Order("id ASC").Find(&events).Error
	assert.NoError(err)
	assert.Len(events, 4)

	eventTypes := make([]string, len(events))
	for i, e := range events {
		eventTypes[i] = e.EventType
	}
	assert.Contains(eventTypes, entity.ReferralEventType_Signup)
	assert.Contains(eventTypes, entity.ReferralEventType_RefereeSignup)
	assert.Contains(eventTypes, entity.ReferralEventType_BindCard)
	assert.Contains(eventTypes, entity.ReferralEventType_RefereeBindCard)

	// 4. Verify total budget: 4 rewards x $5 = $20
	list, err := referralModel.ListCampaigns()
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(20), list.UsedBudgetAll)

	// 5. Verify A's referral stats
	stats, err := referralModel.GetReferralStats(1, false)
	assert.NoError(err)
	assert.Equal(decimal.NewFromInt(10), stats.TotalEarned) // Only referrer rewards counted in referral_code stats
	assert.Len(stats.Events, 4)
}
