package tests

import (
	"fmt"
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/model/payment/reconciliation"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
)

type ARTableSuite struct {
	tests.BillingTestSuite
}

func TestARTable(t *testing.T) {
	suite.Run(t, new(ARTableSuite))
}

func (s *ARTableSuite) SetupTest() {
	s.BillingTestSuite.SetupTest()
}

func (s *ARTableSuite) TestSaveARTableToDB_EmptyRows() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}

	err := model.SaveARTableToDB(nil, time.Now())
	assert.NoError(err)

	err = model.SaveARTableToDB([]*reconciliation.ARTableRow{}, time.Now())
	assert.NoError(err)
}

func (s *ARTableSuite) TestSaveARTableToDB_SingleRow() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	rows := []*reconciliation.ARTableRow{
		{
			TenantID:              "1",
			PaymentCode:           "cus_test",
			PaymentType:           entity.PaymentType_STRIPE,
			BillMonth:             "2026-06",
			StartDate:             "2026-06-01",
			EndDate:               "2026-06-30",
			BindedBillingConfigID: 1,
			IsEstimated:           false,
			InvoiceAmount:         "100",
			TotalAmount:           "100",
		},
	}

	err := model.SaveARTableToDB(rows, date)
	assert.NoError(err)

	// Verify it was saved
	records, err := model.ListARTableFromDB(2026, 6, nil)
	assert.NoError(err)
	assert.Len(records, 1)
	assert.Equal("100", records[0].InvoiceAmount)
	assert.False(records[0].IsEstimated)
}

func (s *ARTableSuite) TestSaveARTableToDB_BatchUpsert() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}
	date := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)

	// Create 250 rows to test batching (batch size is 100)
	rows := make([]*reconciliation.ARTableRow, 250)
	for i := range rows {
		rows[i] = &reconciliation.ARTableRow{
			TenantID:              fmt.Sprint(i + 1),
			PaymentCode:           "cus_test",
			PaymentType:           entity.PaymentType_STRIPE,
			BillMonth:             "2026-05",
			StartDate:             "2026-05-01",
			EndDate:               "2026-05-31",
			BindedBillingConfigID: 1,
			IsEstimated:           true,
			InvoiceAmount:         "50.00",
			TotalAmount:           "50.00",
		}
	}

	err := model.SaveARTableToDB(rows, date)
	assert.NoError(err)

	// Verify all records were saved
	estimated := true
	records, err := model.ListARTableFromDB(2026, 5, &estimated)
	assert.NoError(err)
	assert.Len(records, 250)
}

func (s *ARTableSuite) TestSaveARTableToDB_Upsert() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}
	date := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)

	// Insert initial row
	rows := []*reconciliation.ARTableRow{
		{
			TenantID:              "1",
			PaymentCode:           "cus_test",
			PaymentType:           entity.PaymentType_STRIPE,
			BillMonth:             "2026-04",
			StartDate:             "2026-04-01",
			EndDate:               "2026-04-30",
			BindedBillingConfigID: 1,
			IsEstimated:           true,
			InvoiceAmount:         "100",
			TotalAmount:           "100",
		},
	}
	err := model.SaveARTableToDB(rows, date)
	assert.NoError(err)

	// Upsert with updated amount
	rows[0].InvoiceAmount = "200"
	rows[0].TotalAmount = "200"
	err = model.SaveARTableToDB(rows, date)
	assert.NoError(err)

	// Verify the record was updated, not duplicated
	estimated := true
	records, err := model.ListARTableFromDB(2026, 4, &estimated)
	assert.NoError(err)
	assert.Len(records, 1)
	assert.Equal("200", records[0].InvoiceAmount)
}

func (s *ARTableSuite) TestSaveARTableToDB_DeleteBatch() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}
	date := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)

	// Create 150 rows
	rows := make([]*reconciliation.ARTableRow, 150)
	for i := range rows {
		rows[i] = &reconciliation.ARTableRow{
			TenantID:              fmt.Sprint(i + 1),
			PaymentCode:           "cus_del",
			PaymentType:           entity.PaymentType_STRIPE,
			BillMonth:             "2026-03",
			StartDate:             "2026-03-01",
			EndDate:               "2026-03-31",
			BindedBillingConfigID: 1,
			IsEstimated:           true,
			InvoiceAmount:         "10.00",
			TotalAmount:           "10.00",
		}
	}

	err := model.SaveARTableToDB(rows, date)
	assert.NoError(err)

	// Clean up each record
	estimated := true
	records, err := model.ListARTableFromDB(2026, 3, &estimated)
	assert.NoError(err)
	for _, r := range records {
		err = model.DeleteARTableRecord(r.ID)
		assert.NoError(err)
	}

	// Verify all deleted
	records, err = model.ListARTableFromDB(2026, 3, &estimated)
	assert.NoError(err)
	assert.Len(records, 0)
}

func (s *ARTableSuite) TestSubDecimal() {
	assert := s.Assert()
	model := &reconciliation.ARTableModel{}

	// Indirect test via RenderARTable
	rows := []*reconciliation.ARTableRow{
		{
			TenantID:              "1",
			PaymentCode:           "cus_test",
			PaymentType:           entity.PaymentType_STRIPE,
			BillMonth:             "2026-06",
			StartDate:             "2026-06-01",
			EndDate:               "2026-06-30",
			InvoiceAmount:         "100",
			ChannelLossAmount:     "20",
			RawDedicatedAmount:    "60",
			DedicatedAllocatedLoss: "10",
		},
	}

	csv, err := model.RenderARTable(rows)
	assert.NoError(err)
	str := string(csv)
	// InvoiceAmount - ChannelLossAmount = 100 - 20 = 80
	assert.Contains(str, "80")
	// DedicatedAmount = RawDedicatedAmount - DedicatedAllocatedLoss = 60 - 10 = 50
	assert.Contains(str, "50")
}

// Clean up test data after each test
func (s *ARTableSuite) TearDownTest() {
	db.GetDB().Where("bill_month IN ?", []string{"2026-03", "2026-04", "2026-05", "2026-06"}).
		Delete(&entity.ARTableRecord{})
	s.BillingTestSuite.TearDownTest()
}

// TestTenantCommitmentCostForSupport_IgnoresNonSupportOwner pins down the rule
// that only support-purpose commitment invoices should backfill
// totalSupportCost in the AR table estimate. Today the lookup helper at
// model/payment/reconciliation/ar_table.go::tenantCommitmentCostForSupport
// sums every InvoiceType_COMMITMENT row without inspecting its Owner, so a
// non-support commitment (compute, serverless, future SKUs sharing the same
// invoice type) would be mis-attributed to support.
//
// This test seeds two commitment invoices in the same tenant/billing-config
// pair: one owned by support (should flow through), one owned by an unrelated
// SKU (must be ignored). The expected return value is exactly the support
// owner's cost. Until the helper JOINs commitment_records and filters by
// Owner, this test fails — it is the red that motivates the owner-filter PR.
func (s *ARTableSuite) TestTenantCommitmentCostForSupport_IgnoresNonSupportOwner() {
	assert := s.Assert()

	const (
		tenantID                uint = 9001
		supportBillingConfigID  uint = 7001
		nonSupportBillingConfig uint = 7002
		supportOwner                  = "support-plan:commitment"
		nonSupportOwner               = "compute-plan:commitment" // unrelated SKU
	)
	invoiceDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	// Each invoice is paired 1:1 with its CommitmentRecords row on
	// (BindedBillingConfigID, Date) so the helper's LEFT JOIN produces a
	// single row per invoice and the owner filter can be evaluated cleanly.
	supportInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           invoiceDate,
		CostPrice:             decimal.NewFromInt(120),
		RawPrice:              decimal.NewFromInt(120),
		BindedBillingConfigID: supportBillingConfigID,
	}
	nonSupportInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           invoiceDate,
		CostPrice:             decimal.NewFromInt(500),
		RawPrice:              decimal.NewFromInt(500),
		BindedBillingConfigID: nonSupportBillingConfig,
	}
	assert.NoError(supportInvoice.Create(db.GetDB()))
	assert.NoError(nonSupportInvoice.Create(db.GetDB()))

	supportRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(120),
		Owner:                 supportOwner,
		Date:                  invoiceDate,
		BindedBillingConfigID: supportBillingConfigID,
	}
	nonSupportRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(500),
		Owner:                 nonSupportOwner,
		Date:                  invoiceDate,
		BindedBillingConfigID: nonSupportBillingConfig,
	}
	assert.NoError(supportRecord.Create(db.GetDB()))
	assert.NoError(nonSupportRecord.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id IN ?", []uint{supportBillingConfigID, nonSupportBillingConfig}).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.TenantCommitmentCostForSupport(tenantID, startDate, endDate)
	assert.NoError(err)

	// Expectation: only the support-owned commitment ($120) flows through.
	// The compute-owned $500 must be filtered out. Until the helper learns
	// about Owner, this assertion fails (the helper returns $620 = 120+500).
	assert.True(got.Equal(decimal.NewFromInt(120)),
		"expected only support-purpose commitment cost (120) to flow through, got %s", got.String())
}

// TestTenantCommitmentCostForSupport_AcceptsLegacyInvoiceWithoutCommitmentRecord
// covers the explicit-owner-only path: a commitment invoice with no matching
// commitment_records row at all is excluded. There is no back-compat
// fallback that assumes "support" when Owner is absent.
func (s *ARTableSuite) TestTenantCommitmentCostForSupport_AcceptsLegacyInvoiceWithoutCommitmentRecord() {
	assert := s.Assert()

	const (
		tenantID              uint = 9002
		bindedBillingConfigID uint = 7010
	)
	invoiceDate := time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC)
	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	legacyInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           invoiceDate,
		CostPrice:             decimal.NewFromInt(75),
		RawPrice:              decimal.NewFromInt(75),
		BindedBillingConfigID: bindedBillingConfigID,
	}
	assert.NoError(legacyInvoice.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.TenantCommitmentCostForSupport(tenantID, startDate, endDate)
	assert.NoError(err)
	assert.True(got.Equal(decimal.Zero),
		"invoice without commitment_records must be excluded, got %s", got.String())
}

// TestTenantCommitmentCostForSupport_AcceptsEmptyOwner covers the rule
// that commitment_records.Owner = "" is treated as unspecified and
// excluded — only explicit support-plan:commitment is accepted.
func (s *ARTableSuite) TestTenantCommitmentCostForSupport_AcceptsEmptyOwner() {
	assert := s.Assert()

	const (
		tenantID              uint = 9003
		bindedBillingConfigID uint = 7020
	)
	invoiceDate := time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC)
	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	legacyInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           invoiceDate,
		CostPrice:             decimal.NewFromInt(33),
		RawPrice:              decimal.NewFromInt(33),
		BindedBillingConfigID: bindedBillingConfigID,
	}
	assert.NoError(legacyInvoice.Create(db.GetDB()))

	legacyRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(33),
		Owner:                 "", // legacy / unspecified
		Date:                  invoiceDate,
		BindedBillingConfigID: bindedBillingConfigID,
	}
	assert.NoError(legacyRecord.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", bindedBillingConfigID).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.TenantCommitmentCostForSupport(tenantID, startDate, endDate)
	assert.NoError(err)
	assert.True(got.Equal(decimal.Zero),
		"empty-owner commitment must be excluded, got %s", got.String())
}

// TestQueryTenantBillingPairsForMonth_IncludesSupportOnlyTenant pins down the
// rule that a tenant whose only AR-relevant cost is the support plan fee
// (i.e. no serverless / dedicated / starter / essential / premium / lake
// usage, only support-plan SKUs) MUST still appear in the candidate set
// used by ListEstimatedPayments. Otherwise the AR estimate silently skips
// support-only orgs.
//
// Background: support plan rows are written via SecondaryMeasure with
// internal=true (because the support SKUs have no matching BindedPackage,
// see model/billing/measure.go::procRawUnitCost line ~403). The original
// filter `internal = false` therefore excluded them, masking every
// support-only tenant from the estimate. The fix is the OR branch on
// unit LIKE 'support-plan:%'.
func (s *ARTableSuite) TestQueryTenantBillingPairsForMonth_IncludesSupportOnlyTenant() {
	assert := s.Assert()

	const (
		tenantID              uint = 9100
		bindedBillingConfigID uint = 7100
	)
	measureDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	start := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	// Seed the row exactly as SendSupportPlanMetering / SecondaryMeasure
	// would: unit = support-plan:fixed_price, internal=true, status=DONE.
	supportRow := &entity.UserUnitCost{
		CostID:                "support-plan-normal-9100-fixed",
		TenantID:              tenantID,
		BindedBillingConfigID: bindedBillingConfigID,
		Unit:                  constant.FixedPrice_SKU,
		Cost:                  100,
		CostTimeStamp:         measureDate.Unix(),
		Status:                entity.UserUnitCostStatus_DONE,
		Internal:              true,
	}
	assert.NoError(supportRow.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ? AND cost_id = ?", tenantID, "support-plan-normal-9100-fixed").Delete(&entity.UserUnitCost{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.QueryTenantBillingPairsForMonth(start, end, nil)
	assert.NoError(err)
	_, ok := got[reconciliation.ARTableTenantBillingPair{TenantID: tenantID, BindedBillingConfigID: bindedBillingConfigID}]
	assert.True(ok, "support-only tenant must appear in candidate set, got %v", got)
}

// TestQueryTenantBillingPairsForMonth_ExcludesInternalNonSupportRow makes
// sure the OR branch is scoped tightly: a row with internal=true but a
// non-support unit (e.g. an internal test tenant's stray cost) must still
// be filtered out, just as before.
func (s *ARTableSuite) TestQueryTenantBillingPairsForMonth_ExcludesInternalNonSupportRow() {
	assert := s.Assert()

	const (
		tenantID              uint = 9101
		bindedBillingConfigID uint = 7101
	)
	measureDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	start := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	internalRow := &entity.UserUnitCost{
		CostID:                "internal-non-support-9101",
		TenantID:              tenantID,
		BindedBillingConfigID: bindedBillingConfigID,
		Unit:                  "node:aws:us-east-1:DEPLOY_TYPE_DEDICATED:m5.large", // unrelated SKU
		Cost:                  100,
		CostTimeStamp:         measureDate.Unix(),
		Status:                entity.UserUnitCostStatus_DONE,
		Internal:              true,
	}
	assert.NoError(internalRow.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ? AND cost_id = ?", tenantID, "internal-non-support-9101").Delete(&entity.UserUnitCost{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.QueryTenantBillingPairsForMonth(start, end, nil)
	assert.NoError(err)
	_, ok := got[reconciliation.ARTableTenantBillingPair{TenantID: tenantID, BindedBillingConfigID: bindedBillingConfigID}]
	assert.False(ok, "internal non-support row must not leak through, got %v", got)
}

// TestQueryTenantBillingPairsForMonth_IncludesSupportOnlyTenantWithoutMetering
// pins down the rule that a support-only tenant whose SendSupportPlanMetering
// has NOT yet written a user_unit_costs row for the current month (typical:
// the AR estimate runs at the start of the next month, before the
// support-plan metering worker has caught up) must still appear in the
// candidate set. This is the bug that motivated the second pass on this
// helper: relying solely on user_unit_costs would drop the tenant until
// the next AR estimate cycle, leaving a one-month gap in the support-only
// orgs' AR table history.
//
// The fix is to also scan binded_support_plans and add any tenant whose
// plan is active (status=ACTIVE) and whose date window intersects the
// estimate month — independent of whether metering has fired.
func (s *ARTableSuite) TestQueryTenantBillingPairsForMonth_IncludesSupportOnlyTenantWithoutMetering() {
	assert := s.Assert()

	const (
		tenantID       uint = 9200
		billingCfgType     = "DirectCustomer"
	)
	activeDate := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
	billingStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	start := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	// Seed the billing_config that should receive the AR row, and the
	// BindedSupportPlan that anchors the tenant as "support-only". No
	// user_unit_costs row is created — that is the bug scenario.
	billingCfg := &entity.BillingConfig{
		TenantID:        tenantID,
		StartActiveTime: billingStart,
		Type:            billingCfgType,
		Status:          "Active",
	}
	assert.NoError(billingCfg.Create(db.GetDB()))

	plan := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: 1,
		ActiveDate:    activeDate,
	}
	assert.NoError(plan.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BillingConfig{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.QueryTenantBillingPairsForMonth(start, end, nil)
	assert.NoError(err)
	_, ok := got[reconciliation.ARTableTenantBillingPair{TenantID: tenantID, BindedBillingConfigID: billingCfg.ID}]
	assert.True(ok, "support-only tenant without metering row must still appear via BindedSupportPlan lookup, got %v", got)
}

// TestEstimateSupportFeeForTenant_FixedPrice_FullMonth locks down the
// "estimate is independent of metering" rule for the FixedPrice plan
// type. The plan is active the whole month and the estimate is run on
// the last day of the month; the support fee is the full monthly
// price. The estimate is computed even when no user_unit_costs rows
// exist (this is the "early-month estimate" case).
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_FixedPrice_FullMonth() {
	assert := s.Assert()

	const (
		tenantID       uint = 9300
		supportPlanID  uint = 8001
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             entity.SupportPlanType_FixedPrice,
		FixedChargePrice: decimal.NewFromInt(300),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	// 30-day month, 30 days elapsed, dailyRate = 300/30 = 10, expected = 300
	assert.True(got.Equal(decimal.NewFromInt(300)),
		"full-month FixedPrice must estimate to the full $300, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_FixedPrice_MidMonth prorates a
// FixedPrice plan that was activated on the 15th against the full
// active window in the month (15–30, 16 days inclusive), regardless of
// when the estimate runs.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_FixedPrice_MidMonth() {
	assert := s.Assert()

	const (
		tenantID      uint = 9301
		supportPlanID uint = 8002
	)
	activeDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             entity.SupportPlanType_FixedPrice,
		FixedChargePrice: decimal.NewFromInt(300),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	// Plan active 15–30 (16 days, inclusive). The estimate pins
	// nowCapped to planEnd (June 30) so the active window is fully
	// captured regardless of when the estimate runs — dailyRate × 16
	// days = 10 × 16 = 160. Previously the estimate charged only the
	// elapsed days (June 15 → 20 = 6 days, 60), which silently
	// under-billed the customer.
	assert.True(got.Equal(decimal.NewFromInt(160)),
		"mid-month FixedPrice must prorate by full active window in month, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_Percentage_FloorWins pins down the
// "estimate cheat" rule: when the percentage cost to date is far below
// the commitment floor (so extrapolating to a full month would be
// higher than the floor), the floor is the worst-case estimate. The
// plan is active the whole month so the floor is NOT prorated.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_Percentage_FloorWins() {
	assert := s.Assert()

	const (
		tenantID      uint = 9302
		supportPlanID uint = 8003
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) // 1st

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Commitment floor = $129 (a single commitment invoice in the month).
	commitmentInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC),
		CostPrice:             decimal.NewFromInt(129),
		RawPrice:              decimal.NewFromInt(129),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentInvoice.Create(db.GetDB()))

	commitmentRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(129),
		Owner:                 "support-plan:commitment",
		Date:                  time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentRecord.Create(db.GetDB()))

	// No user_unit_costs at all — simulating the "1st of the month, no
	// metering has happened yet" case. percentage cost is therefore $0.
	// But the floor ($129) wins.

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", 0).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	assert.True(got.Equal(decimal.NewFromInt(129)),
		"PercentagePrice with no usage must fall back to the $129 floor, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_Percentage_ExtrapolatedWins pins down
// the inverse of FloorWins: when the percentage cost (cumulative
// usage × pct) is HIGHER than the commitment floor, the percentage
// cost wins. The estimate should not silently cap at the floor.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_Percentage_ExtrapolatedWins() {
	assert := s.Assert()

	const (
		tenantID      uint = 9305
		supportPlanID uint = 8006
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Small commitment floor ($10) — far below what 20% of the seed
	// usage would yield. Floor must NOT cap the percentage side.
	commitmentInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		CostPrice:             decimal.NewFromInt(10),
		RawPrice:              decimal.NewFromInt(10),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentInvoice.Create(db.GetDB()))

	commitmentRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(10),
		Owner:                 "support-plan:commitment",
		Date:                  time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentRecord.Create(db.GetDB()))

	// Seed $1000 of non-support usage. With 20% pct → $200 estimate.
	// Floor ($10) is much smaller, so the percentage wins outright.
	usageRow := &entity.UserUnitCost{
		CostID:                "pct-extrapolated-usage",
		TenantID:              tenantID,
		BindedBillingConfigID: 0,
		Unit:                  "node:aws:us-east-1:DEPLOY_TYPE_DEDICATED:m5.large",
		Cost:                  1000,
		CostTimeStamp:         date.Add(8 * time.Hour).Unix(),
		Status:                entity.UserUnitCostStatus_DONE,
		Internal:              false,
		CostPrice:             decimal.NewFromInt(1000),
	}
	assert.NoError(usageRow.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", 0).Delete(&entity.CommitmentRecords{})
		db.GetDB().Where("tenant_id = ? AND cost_id = ?", tenantID, "pct-extrapolated-usage").Delete(&entity.UserUnitCost{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	assert.True(got.Equal(decimal.NewFromInt(200)),
		"PercentagePrice extrapolated cost ($1000 × 20% = $200) must beat the $10 floor, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_Percentage_PartialMonth_FloorProrated
// pins down that the commitment floor prorates by active days for
// partial-month plans. A plan activated on the 15th and seeded with a
// full-month floor of $300 must report max(percentage_cost,
// $300 × 16/30 = $160).
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_Percentage_PartialMonth_FloorProrated() {
	assert := s.Assert()

	const (
		tenantID      uint = 9306
		supportPlanID uint = 8007
	)
	activeDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Full-month floor $300 — proration should give 300 × 16/30 = $160.
	commitmentInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		CostPrice:             decimal.NewFromInt(300),
		RawPrice:              decimal.NewFromInt(300),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentInvoice.Create(db.GetDB()))

	commitmentRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(300),
		Owner:                 "support-plan:commitment",
		Date:                  time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentRecord.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", 0).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	// floor prorated = 300 * 16 / 30 = 160
	// percentage cost = 0 (no usage seeded yet) → 0 < 160, so floor wins.
	assert.True(got.Equal(decimal.NewFromInt(160)),
		"Partial-month PercentagePrice floor must prorate to $160, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_UnknownPlanType_ReturnsError pins
// down that the estimate refuses to silently fall back to the
// commitment floor for an unrecognized SupportPlanType. A new
// SupportPlanType should land with an explicit case here, not get
// absorbed silently into "the contract is the contract".
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_UnknownPlanType_ReturnsError() {
	assert := s.Assert()

	const (
		tenantID      uint = 9307
		supportPlanID uint = 8008
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             "support_plan:experimental_v2", // not in the supported set
		FixedChargePrice: decimal.NewFromInt(50),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.Error(err, "unknown plan type must surface as an error, not silently use the floor")
	assert.True(got.IsZero(),
		"unknown plan type must return zero estimate, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_NoActivePlan returns 0 when the tenant
// has no active support plan at all.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_NoActivePlan() {
	assert := s.Assert()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(9999, 0, time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC))
	assert.NoError(err)
	assert.True(got.IsZero(), "no active plan must yield zero, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_PlanOutsideMonth covers a plan whose
// active window is entirely outside the estimate month — it must be
// skipped.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_PlanOutsideMonth() {
	assert := s.Assert()

	const (
		tenantID      uint = 9303
		supportPlanID uint = 8004
	)
	// Plan active next month, not the estimate month.
	activeDate := time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             entity.SupportPlanType_FixedPrice,
		FixedChargePrice: decimal.NewFromInt(300),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, 0, date)
	assert.NoError(err)
	assert.True(got.IsZero(), "plan whose active window is outside the month must be skipped, got %s", got.String())
}

// TestEstimateSupportFeeForTenant_TieredPrice_TwoTiers_TieredCostWins
// pins down the unit-mismatch fix that the post-review refactor landed:
// cumulative usage to date (raw dollars) is scaled by
// SupportPlan_PercentagePriceBase before being passed to
// marketing.TieredPricing, so it lives in the same unit space as
// previousCumulative (scaled, from SupportPlanMeteringRecords.Usage)
// and the rule bounds (scaled inside TieredPricing). Without the scale,
// the lowerBound comparison always short-circuits and only the first
// tier ever fires.
//
// Setup: tenant has accumulated $1,500 of non-support usage (scaled
// = 1,500,000) by June 30. The plan has two tiers:
//   - tier 1: lower=0,        upper=1000,  rate=0.10  (raw dollars)
//   - tier 2: lower=1000,     upper=2000,  rate=0.05
// Total estimate = 1000 * 0.10 + 500 * 0.05 = 100 + 25 = $125.
// No commitment floor is seeded, so the tiered cost wins outright.
func (s *ARTableSuite) TestEstimateSupportFeeForTenant_TieredPrice_TwoTiers_TieredCostWins() {
	assert := s.Assert()

	const (
		tenantID        uint = 9304
		supportPlanID   uint = 8005
		billingConfigID uint = 7304
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	estimateDate := time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)

	// Two tiers: 0..1000 @ 10%, 1000..2000 @ 5%.
	tiers := entity.TieredPriceRules{
		{LowerBound: decimal.Zero, UpperBound: decimalPtr(decimal.NewFromInt(1000)), SupportPricePercentage: decimal.RequireFromString("0.10")},
		{LowerBound: decimal.NewFromInt(1000), UpperBound: decimalPtr(decimal.NewFromInt(2000)), SupportPricePercentage: decimal.RequireFromString("0.05")},
	}
	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             entity.SupportPlanType_TieredPrice,
		TieredPriceRules: tiers,
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Seed $1500 of non-support usage (cost_price scaled = 1500*1000
	// for cumulative purposes). The unit is "node:aws:us-east-1:..." so
	// it is excluded from the support SKUs and from the usage base.
	usageRow := &entity.UserUnitCost{
		CostID:                "tiered-usage-9304",
		TenantID:              tenantID,
		BindedBillingConfigID: billingConfigID,
		Unit:                  "node:aws:us-east-1:DEPLOY_TYPE_DEDICATED:m5.large",
		Cost:                  1500,
		CostTimeStamp:         estimateDate.Add(8 * time.Hour).Unix(), // 08:00 UTC on the 30th
		Status:                entity.UserUnitCostStatus_DONE,
		Internal:              false,
		CostPrice:             decimal.NewFromInt(1500),
	}
	assert.NoError(usageRow.Create(db.GetDB()))

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND cost_id = ?", tenantID, "tiered-usage-9304").Delete(&entity.UserUnitCost{})
	}()

	model := &reconciliation.ARTableModel{}
	got, err := model.EstimateSupportFeeForTenant(tenantID, billingConfigID, estimateDate)
	assert.NoError(err)
	// tier1: 1000 * 0.10 = 100
	// tier2: 500  * 0.05 = 25
	// total = 125 (no commitment floor seeded, so the floor is 0)
	assert.True(got.Equal(decimal.NewFromInt(125)),
		"tiered price across two tiers must compute 1000*0.10 + 500*0.05 = 125, got %s", got.String())
}

// TestEstimateSupportForRow_FullSupport_PercentageFloorWins pins down
// the new design's full-support path: base is read from the AR table's
// own non-support raw_* columns + dedicatedAccrual; commitment is the
// floor. Contract gap A = max(0, E - row.RawSupportAmount).
func (s *ARTableSuite) TestEstimateSupportForRow_FullSupport_PercentageFloorWins() {
	assert := s.Assert()

	const (
		tenantID      uint = 9500
		supportPlanID uint = 8200
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Commitment floor = $300
	commitmentInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC),
		CostPrice:             decimal.NewFromInt(300),
		RawPrice:              decimal.NewFromInt(300),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentInvoice.Create(db.GetDB()))

	commitmentRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(300),
		Owner:                 "support-plan:commitment",
		Date:                  time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentRecord.Create(db.GetDB()))

	row := &reconciliation.ARTableRow{
		TenantID:               "9500",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "50",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "100",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "50", // metering so far (partial month)
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", 0).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	E, A, err := model.EstimateSupportForRow(tenantID, 0, date, row)
	assert.NoError(err)
	// Base = 100 (raw_dedicated) + 50 (dedicatedAccrual) = 150.
	// Percentage cost = 150 × 0.20 = 30.
	// Floor = 300 (full commitment, full-month plan).
	// E = max(30, 300) = 300.
	assert.True(E.Equal(decimal.NewFromInt(300)),
		"full-support floor wins: E=300, got %s", E.String())
	// A = max(0, 300 - 50) = 250 (contract gap = the amount beyond the
	// metering that the customer still owes).
	assert.True(A.Equal(decimal.NewFromInt(250)),
		"contract gap A=250, got %s", A.String())
}

// TestEstimateSupportForRow_FullSupport_PercentageWins pins down the
// inverse: when the percentage cost (base * pct) is HIGHER than the
// floor, the percentage wins.
func (s *ARTableSuite) TestEstimateSupportForRow_FullSupport_PercentageWins() {
	assert := s.Assert()

	const (
		tenantID      uint = 9501
		supportPlanID uint = 8201
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Small commitment floor ($10) — far below what 20% of the seed base
	// would yield. Floor must NOT cap the percentage side.
	commitmentInvoice := &entity.Invoice{
		TenantID:              tenantID,
		Type:                  entity.InvoiceType_COMMITMENT,
		Status:                entity.InvoiceStatus_GENERATED,
		InvoiceDate:           time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		CostPrice:             decimal.NewFromInt(10),
		RawPrice:              decimal.NewFromInt(10),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentInvoice.Create(db.GetDB()))

	commitmentRecord := &entity.CommitmentRecords{
		Amount:                decimal.NewFromInt(10),
		Owner:                 "support-plan:commitment",
		Date:                  time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC),
		BindedBillingConfigID: 0,
	}
	assert.NoError(commitmentRecord.Create(db.GetDB()))

	row := &reconciliation.ARTableRow{
		TenantID:               "9501",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "0",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "1000",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "0",
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
		db.GetDB().Where("tenant_id = ? AND type = ?", tenantID, entity.InvoiceType_COMMITMENT).Delete(&entity.Invoice{})
		db.GetDB().Where("binded_billing_config_id = ?", 0).Delete(&entity.CommitmentRecords{})
	}()

	model := &reconciliation.ARTableModel{}
	E, A, err := model.EstimateSupportForRow(tenantID, 0, date, row)
	assert.NoError(err)
	// Base = 1000 (raw_dedicated) + 0 (dedicatedAccrual) = 1000.
	// Percentage cost = 1000 × 0.20 = 200.
	// Floor = 10.
	// E = max(200, 10) = 200.
	assert.True(E.Equal(decimal.NewFromInt(200)),
		"full-support percentage wins: E=200, got %s", E.String())
	// A = max(0, 200 - 0) = 200.
	assert.True(A.Equal(decimal.NewFromInt(200)),
		"contract gap A=200, got %s", A.String())
}

// TestEstimateSupportForRow_FullSupport_NoActivePlan pins down that
// when there's no active plan for the tenant, E = 0 and A = 0.
func (s *ARTableSuite) TestEstimateSupportForRow_FullSupport_NoActivePlan() {
	assert := s.Assert()

	const tenantID uint = 9502
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	row := &reconciliation.ARTableRow{
		TenantID:               "9502",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "0",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "0",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "0",
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	model := &reconciliation.ARTableModel{}
	E, A, err := model.EstimateSupportForRow(tenantID, 0, date, row)
	assert.NoError(err)
	assert.True(E.IsZero(), "E=0 when no plan, got %s", E.String())
	assert.True(A.IsZero(), "A=0 when no plan, got %s", A.String())
}

// TestEstimateSupportForRow_PartialSupport_FixedProrates pins down
// that for a partial-month plan, the fixed charge prorates by the
// plan's active window regardless of the AR row's full-month raw_*
// values.
func (s *ARTableSuite) TestEstimateSupportForRow_PartialSupport_FixedProrates() {
	assert := s.Assert()

	const (
		tenantID      uint = 9503
		supportPlanID uint = 8202
	)
	activeDate := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC) // mid-month
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:            gorm.Model{ID: supportPlanID},
		Type:             entity.SupportPlanType_FixedPrice,
		FixedChargePrice: decimal.NewFromInt(300),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// AR row's raw values are full-month-looking (irrelevant for fixed).
	// For FixedPrice, partial support should just prorate the fixed
	// charge by activeDays/daysInMonth.
	row := &reconciliation.ARTableRow{
		TenantID:               "9503",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "0",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "0",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "0",
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	E, A, err := model.EstimateSupportForRow(tenantID, 0, date, row)
	assert.NoError(err)
	// Plan active 15–30 (16 days, inclusive). 30-day June.
	// E = 300 × 16/30 = 160.
	assert.True(E.Equal(decimal.RequireFromString("160")),
		"partial-support FixedPrice prorates to 160, got %s", E.String())
	assert.True(A.Equal(decimal.RequireFromString("160")),
		"contract gap A=160 (no metering), got %s", A.String())
}

// TestEstimateSupportForRow_SkipsUnknownPlanType pins down that
// EstimateSupportForRow does NOT fail the entire batch when one of
// the tenant's plans has an unrecognised SupportPlanType. The bad
// plan is skipped, the others still contribute to E. This is the
// "skip and continue" behaviour added to prevent one stale plan
// from blowing up the whole AR table for every tenant.
func (s *ARTableSuite) TestEstimateSupportForRow_SkipsUnknownPlanType() {
	assert := s.Assert()

	const (
		tenantID         uint = 9601
		goodPlanID       uint = 8301
		unknownPlanID    uint = 8302
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	// Good plan: percentage 20%.
	goodPlan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: goodPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.20"),
	}
	assert.NoError(goodPlan.Create(db.GetDB()))

	// Bad plan: unknown type. estimateOnePlanContract will return an error.
	unknownPlan := &entity.SupportPlan{
		Model:            gorm.Model{ID: unknownPlanID},
		Type:             "support_plan:experimental_v2",
		FixedChargePrice: decimal.NewFromInt(50),
	}
	assert.NoError(unknownPlan.Create(db.GetDB()))

	bindedGood := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: goodPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(bindedGood.Create(db.GetDB()))

	bindedUnknown := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: unknownPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(bindedUnknown.Create(db.GetDB()))

	row := &reconciliation.ARTableRow{
		TenantID:               "9601",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "0",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "1000",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "0",
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		db.GetDB().Where("id IN ?", []uint{goodPlanID, unknownPlanID}).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	E, A, err := model.EstimateSupportForRow(tenantID, 0, date, row)

	// Must not error: the unknown-type plan should be skipped silently.
	assert.NoError(err, "unknown plan type must NOT fail the row")

	// Base = 1000 (raw_dedicated) + 0 (dedicatedAccrual) = 1000.
	// Good plan: percentage cost = 1000 × 0.20 = 200.
	// Bad plan: skipped (no contribution).
	// contractTotal = 200. Floor = 0 (no commitment invoices).
	// E = max(200, 0) = 200.
	assert.True(E.Equal(decimal.NewFromInt(200)),
		"good plan contributes 200, unknown plan skipped, got E=%s", E.String())
	// A = max(0, 200 - 0) = 200.
	assert.True(A.Equal(decimal.NewFromInt(200)),
		"contract gap A=200, got %s", A.String())
}

// TestEstimateSupportForRow_LoadsSoftDeletedSupportPlan pins down
// that the SupportPlan preload uses Unscoped(), so a BindedSupportPlan
// pointing at a soft-deleted SupportPlan template still has its Type
// field populated. Without Unscoped(), preload silently returns a
// zero-value SupportPlan whose Type is "", which estimateOnePlanContract
// would then reject.
func (s *ARTableSuite) TestEstimateSupportForRow_LoadsSoftDeletedSupportPlan() {
	assert := s.Assert()

	const (
		tenantID      uint = 9602
		supportPlanID uint = 8303
	)
	activeDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)

	plan := &entity.SupportPlan{
		Model:                  gorm.Model{ID: supportPlanID},
		Type:                   entity.SupportPlanType_PercentagePrice,
		SupportPricePercentage: decimal.RequireFromString("0.10"),
	}
	assert.NoError(plan.Create(db.GetDB()))

	binded := &entity.BindedSupportPlan{
		TenantID:      tenantID,
		Status:        entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlanID: supportPlanID,
		ActiveDate:    activeDate,
	}
	assert.NoError(binded.Create(db.GetDB()))

	// Soft-delete the support plan template. BindedSupportPlan still
	// references it and is still active.
	assert.NoError(db.GetDB().Delete(plan).Error)
	// Verify the soft-delete landed.
	var deletedPlan entity.SupportPlan
	assert.Error(db.GetDB().Where("id = ?", supportPlanID).First(&deletedPlan).Error,
		"plan should be soft-deleted before assertion")

	row := &reconciliation.ARTableRow{
		TenantID:               "9602",
		BindedBillingConfigID:  0,
		DedicatedAccrualAmount: "0",
		RawServerlessAmount:    "0",
		RawDedicatedAmount:     "500",
		RawStarterAmount:       "0",
		RawEssentialAmount:     "0",
		RawSupportAmount:       "0",
		RawPremiumAmount:       "0",
		RawLakeAmount:          "0",
	}

	defer func() {
		db.GetDB().Where("tenant_id = ?", tenantID).Delete(&entity.BindedSupportPlan{})
		// Hard-delete the SupportPlan for cleanup since it was soft-deleted.
		db.GetDB().Unscoped().Where("id = ?", supportPlanID).Delete(&entity.SupportPlan{})
	}()

	model := &reconciliation.ARTableModel{}
	E, _, err := model.EstimateSupportForRow(tenantID, 0, date, row)
	assert.NoError(err, "soft-deleted support plan must still be loaded and classified")

	// Base = 500. Percentage = 500 × 0.10 = 50. Floor = 0.
	// E = max(50, 0) = 50.
	assert.True(E.Equal(decimal.NewFromInt(50)),
		"soft-deleted plan still contributes via Unscoped preload, got E=%s", E.String())
}

func decimalPtr(d decimal.Decimal) *decimal.Decimal { return &d }
