package reconciliation

import (
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/assert"
)

func TestSubDecimal(t *testing.T) {
	assert := assert.New(t)

	assert.Equal("0", subDecimal("", ""))
	assert.Equal("-5", subDecimal("", "5"))
	assert.Equal("10", subDecimal("10", ""))
	assert.Equal("5", subDecimal("10", "5"))
	assert.Equal("7.5", subDecimal("10.5", "3"))
	assert.Equal("-3", subDecimal("2", "5"))
}

func TestSerialize(t *testing.T) {
	assert := assert.New(t)

	assert.Equal(`["a","b"]`, serialize([]string{"a", "b"}))
	assert.Equal(`[]`, serialize([]string{}))
	assert.Equal(`[true,false]`, serialize([]bool{true, false}))
	assert.Equal("", serialize(nil))
}

func TestArTableRowToRecord(t *testing.T) {
	assert := assert.New(t)

	row := &ARTableRow{
		TenantID:                           "123",
		PartnerID:                          "partner1",
		PaymentCode:                        "code1",
		PaymentType:                        "stripe",
		BillMonth:                          "2023-09",
		StartDate:                          "2023-09-01",
		EndDate:                            "2023-09-30",
		BindedBillingConfigID:             1,
		IsEstimated:                        false,
		StripeAmount:                       []string{"10", "20"},
		StripeTaxRate:                      []string{"0.1", "0.1"},
		StripeAmountIncludingTax:           []string{"11", "22"},
		StripeInvoiceNumber:                []string{"INV-001", "INV-002"},
		WireTransferAmount:                 "0",
		MarketplaceAmount:                  "0",
		PrepaymentCredits:                  "5",
		TotalAmount:                        "33",
		InvoiceAmount:                      "33",
		StripeInvoice:                      []string{"inv1.pdf", "inv2.pdf"},
		StripeInvoicePaid:                  []bool{true, false},
		StripeInvoiceFinalizedDate:         []string{"2023-09-05", "2023-09-15"},
		StripeInvoicePaidDate:              []string{"2023-09-06", ""},
		RawServerlessAmount:                "10",
		RawDedicatedAmount:                 "20",
		RawStarterAmount:                   "1",
		RawEssentialAmount:                 "2",
		RawSupportAmount:                   "3",
		RawPremiumAmount:                   "4",
		RawLakeAmount:                      "5",
		RawBYOCAmount:                      "6",
		ChannelLossAmount:                  "0",
		DedicatedAccrualAmount:             "0",
		SupportAccrualAmount:               "0",
	}

	record := arTableRowToRecord(row)
	assert.NotNil(record)
	assert.Equal(uint(123), record.TenantID)
	assert.Equal("stripe", record.PaymentType)
	assert.Equal("2023-09", record.BillMonth)
	assert.False(record.IsEstimated)
	assert.Len(record.StripeAmount, 2)
	assert.Equal("10", record.StripeAmount[0])
	// StripeInvoicePaid should be stringified bools
	assert.Len(record.StripeInvoicePaid, 2)
	assert.Equal("true", record.StripeInvoicePaid[0])
	assert.Equal("false", record.StripeInvoicePaid[1])
	// BYOC fields map through
	assert.Equal("6", record.RawBYOCAmount)
}

func TestArTableRecordToResponse(t *testing.T) {
	assert := assert.New(t)

	record := &entity.ARTableRecord{
		TenantID:                 123,
		PartnerID:                "partner1",
		PaymentCode:              "code1",
		PaymentType:              "stripe",
		BillMonth:                "2023-09",
		StartDate:                "2023-09-01",
		EndDate:                  "2023-09-30",
		BindedBillingConfigID:   1,
		IsEstimated:              false,
		StripeAmount:            entity.StringSlice{"10", "20"},
		StripeTaxRate:           entity.StringSlice{"0.1", "0.1"},
		StripeAmountIncludingTax: entity.StringSlice{"11", "22"},
		StripeInvoiceNumber:     entity.StringSlice{"INV-001"},
		WireTransferAmount:       "0",
		MarketplaceAmount:        "0",
		PrepaymentCredits:        "5",
		InvoiceAmount:            "33",
		StripeInvoice:           entity.StringSlice{"inv1.pdf"},
		StripeInvoicePaid:       entity.StringSlice{"true", "false"},
		StripeInvoiceFinalizedDate: entity.StringSlice{"2023-09-05"},
		StripeInvoicePaidDate:   entity.StringSlice{"2023-09-06"},
		RawServerlessAmount:      "10",
		RawDedicatedAmount:       "20",
		RawStarterAmount:         "1",
		RawEssentialAmount:       "2",
		RawSupportAmount:         "3",
		RawPremiumAmount:         "4",
		RawLakeAmount:            "5",
		RawBYOCAmount:            "6",
	}

	resp := arTableRecordToResponse(record)
	assert.NotNil(resp)
	assert.Equal(uint(123), resp.TenantID)
	assert.Equal("stripe", resp.PaymentType)
	assert.Equal("2023-09", resp.BillMonth)
	assert.False(resp.IsEstimated)
	assert.Len(resp.StripeAmount, 2)
	assert.Equal("10", resp.StripeAmount[0])
	// TotalAmount = InvoiceAmount - ChannelLossAmount = 33 - 0 = 33
	assert.Equal("33", resp.TotalAmount)
	// Sub-allocation amounts
	assert.Equal("10", resp.ServerlessAmount)
	assert.Equal("20", resp.DedicatedAmount)
	// BYOC amounts
	assert.Equal("6", resp.RawBYOCAmount)
	assert.Equal("6", resp.BYOCAmount)
}

func TestInitARRow(t *testing.T) {
	assert := assert.New(t)
	date := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	billingConfigID := uint(1)

	payment := &forms.PaymentMethod{
		TenantID:              100,
		PaymentCode:           "cus_test",
		PaymentType:           "stripe",
		BindedBillingConfigID: &billingConfigID,
	}

	row, err := initARRow(payment, date)
	assert.NoError(err)
	assert.Equal("100", row.TenantID)
	assert.Equal("cus_test", row.PaymentCode)
	assert.Equal("stripe", row.PaymentType)
	assert.Equal("2026-06", row.BillMonth)
	assert.Equal("2026-06-01", row.StartDate)
	assert.Equal("2026-06-30", row.EndDate)
	assert.Equal(uint(1), row.BindedBillingConfigID)
}

func TestRenderARTable(t *testing.T) {
	assert := assert.New(t)
	m := &ARTableModel{}

	rows := []*ARTableRow{
		{
			TenantID:         "100",
			PaymentCode:      "cus_test",
			PaymentType:      "stripe",
			BillMonth:        "2026-06",
			StartDate:        "2026-06-01",
			EndDate:          "2026-06-30",
			InvoiceAmount:    "100.50",
			TotalAmount:      "100.50",
			IsEstimated:      true,
			RawServerlessAmount: "30",
			RawDedicatedAmount:  "40",
			RawSupportAmount:    "30.50",
			RawBYOCAmount:       "15",
		},
	}

	csv, err := m.RenderARTable(rows)
	assert.NoError(err)
	assert.NotNil(csv)
	// Should contain header and data
	assert.Contains(string(csv), "bill_month")
	assert.Contains(string(csv), "2026-06")
	assert.Contains(string(csv), "100.50")
	// Serverless amount (30 - 0 allocated loss)
	assert.Contains(string(csv), "30")
	// Dedicated amount (40 - 0 allocated loss)
	assert.Contains(string(csv), "40")
	// Support amount (30.50 - 0 allocated loss)
	assert.Contains(string(csv), "30.5") // trailing zero trimmed by decimal
	// BYOC header and data
	assert.Contains(string(csv), "raw_byoc_amount")
	assert.Contains(string(csv), "byoc_amount")
	assert.Contains(string(csv), "15") // BYOCAmount (15 - 0 allocated loss)
}

func TestProrateCommitment_MidMonth(t *testing.T) {
	assert := assert.New(t)

	mockNow := time.Date(2026, 6, 11, 10, 0, 0, 0, time.UTC)
	timeutils.SetMockNow(mockNow)

	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	commitmentCost := decimal.NewFromInt(29)

	result := prorateCommitment(commitmentCost, startDate, endDate)
	// $29 * 11/30 ≈ $10.6333...
	assert.True(result.GreaterThan(decimal.NewFromFloat(10.63)))
	assert.True(result.LessThan(decimal.NewFromFloat(10.64)))
}

func TestProrateCommitment_FullMonth(t *testing.T) {
	assert := assert.New(t)

	// July 5: June has fully elapsed
	mockNow := time.Date(2026, 7, 5, 10, 0, 0, 0, time.UTC)
	timeutils.SetMockNow(mockNow)

	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	commitmentCost := decimal.NewFromInt(29)

	result := prorateCommitment(commitmentCost, startDate, endDate)
	assert.True(result.Equal(decimal.NewFromInt(29)))
}

func TestProrateCommitment_FirstDay(t *testing.T) {
	assert := assert.New(t)

	mockNow := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	timeutils.SetMockNow(mockNow)

	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	commitmentCost := decimal.NewFromInt(30)

	result := prorateCommitment(commitmentCost, startDate, endDate)
	// $30 * 1/30 = $1.00
	assert.True(result.Equal(decimal.NewFromInt(1)))
}

func TestProrateCommitment_BeforeMonth(t *testing.T) {
	assert := assert.New(t)

	mockNow := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
	timeutils.SetMockNow(mockNow)

	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	commitmentCost := decimal.NewFromInt(29)

	result := prorateCommitment(commitmentCost, startDate, endDate)
	// 0 days elapsed → prorated to 0
	assert.True(result.Equal(decimal.Zero))
}

func TestProrateCommitment_ZeroCommitment(t *testing.T) {
	assert := assert.New(t)

	mockNow := time.Date(2026, 6, 11, 10, 0, 0, 0, time.UTC)
	timeutils.SetMockNow(mockNow)

	startDate := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
	commitmentCost := decimal.Zero

	result := prorateCommitment(commitmentCost, startDate, endDate)
	assert.True(result.Equal(decimal.Zero))
}

func TestSubDecimalEdgeCases(t *testing.T) {
	assert := assert.New(t)

	// Large numbers
	assert.Equal("999999.5", subDecimal("1000000", "0.5"))

	// Very small numbers
	assert.Equal("0.0001", subDecimal("0.0002", "0.0001"))

	// Negative a
	assert.Equal("-15", subDecimal("-10", "5"))

	// Negative b (subtraction of negative = addition)
	assert.Equal("15", subDecimal("10", "-5"))
}

// TestApplySupportDelta_PositiveDelta_UpdatesAllAccumulators pins down the
// core contract: when the post-hoc support fee is HIGHER than the cost
// explorer's metering baseline, the (new - old) delta flows into
// invoice, total, wire_transfer, and marketplace. Without this, the
// cumulative accumulators silently kept the explorer's stale value
// even after raw_support_amount had been overwritten.
func TestApplySupportDelta_PositiveDelta_UpdatesAllAccumulators(t *testing.T) {
	assert := assert.New(t)

	inv, total, wire, market := applySupportDelta(
		"100", "100", "100", "100",
		decimal.RequireFromString("50"),
		entity.PaymentType_WireTransfer,
		true,
	)
	assert.Equal("150", inv)
	assert.Equal("150", total)
	assert.Equal("150", wire)
	// WireTransfer shouldn't touch marketplace; it should pass through.
	assert.Equal("100", market)
}

// TestApplySupportDelta_MarketplacePayments updates marketplace_amount
// (not wire_transfer) for AWS / GCP / AliCloud / AWSCPPOReseller.
func TestApplySupportDelta_MarketplacePayments(t *testing.T) {
	assert := assert.New(t)

	for _, pt := range []string{
		entity.PaymentType_AWS,
		entity.PaymentType_GCP,
		entity.PaymentType_AliCloud,
		entity.PaymentType_AWSCPPOReseller,
	} {
		inv, total, wire, market := applySupportDelta(
			"200", "200", "", "200",
			decimal.RequireFromString("30"),
			pt,
			true,
		)
		assert.Equal("230", inv, "invoice for %s", pt)
		assert.Equal("230", total, "total for %s", pt)
		assert.Equal("", wire, "wire should stay empty for %s", pt)
		assert.Equal("230", market, "marketplace for %s", pt)
	}
}

// TestApplySupportDelta_NoCommitment_OnlyInvoiceUpdates pins down that
// total / wire / marketplace only flow when commitment > 0 (those
// accumulators are empty when commitment = 0 in the cost explorer
// path, so applying delta would corrupt a zero).
func TestApplySupportDelta_NoCommitment_OnlyInvoiceUpdates(t *testing.T) {
	assert := assert.New(t)

	inv, total, wire, market := applySupportDelta(
		"100", "0", "0", "0",
		decimal.RequireFromString("25"),
		entity.PaymentType_WireTransfer,
		false,
	)
	assert.Equal("125", inv)
	assert.Equal("0", total, "total must NOT update without commitment")
	assert.Equal("0", wire, "wire must NOT update without commitment")
	assert.Equal("0", market, "marketplace must NOT update without commitment")
}

// TestApplySupportDelta_NegativeDelta_ClampsToZero guards the rare case
// where the post-hoc support figure is smaller than the metering
// baseline (e.g., a commitment floor bigger than the contract value).
// Wire / marketplace would otherwise go negative, which has no
// business meaning. Clamp to 0.
func TestApplySupportDelta_NegativeDelta_ClampsToZero(t *testing.T) {
	assert := assert.New(t)

	inv, total, wire, market := applySupportDelta(
		"50", "50", "50", "50",
		decimal.RequireFromString("-200"),
		entity.PaymentType_WireTransfer,
		true,
	)
	assert.Equal("-150", inv, "invoice goes negative — that's the truth")
	assert.Equal("-150", total)
	assert.Equal("0", wire, "wire clamps to 0")
	// WireTransfer doesn't touch marketplace — it stays at the input
	// value (which here is "50", but the test is structured around
	// wire/marketplace symmetry; the second test below covers the
	// AWS marketplace clamping path explicitly).
	assert.Equal("50", market, "marketplace unchanged for WireTransfer")

	// AWS marketplace path: same negative delta, but it clamps.
	_, _, _, awsMarket := applySupportDelta(
		"50", "50", "", "50",
		decimal.RequireFromString("-200"),
		entity.PaymentType_AWS,
		true,
	)
	assert.Equal("0", awsMarket, "AWS marketplace clamps to 0")
}

// TestApplySupportDelta_OtherPaymentTypesDontTouchWireOrMarketplace
// verifies that Stripe / Direct / etc. don't accidentally flow into
// wire or marketplace columns. Only WireTransfer touches
// wire_transfer_amount; only AWS/GCP/AliCloud/AWSCPPOReseller touch
// marketplace_amount.
func TestApplySupportDelta_OtherPaymentTypesDontTouchWireOrMarketplace(t *testing.T) {
	assert := assert.New(t)

	for _, pt := range []string{
		entity.PaymentType_STRIPE,
		entity.PaymentType_MSP,
		entity.PaymentType_Resale,
		entity.PaymentType_AZURE,
		entity.PaymentType_WireTransfer, // sanity: only wire, not market
	} {
		inv, total, wire, market := applySupportDelta(
			"100", "100", "100", "",
			decimal.RequireFromString("50"),
			pt,
			true,
		)
		assert.Equal("150", inv, "invoice for %s", pt)
		assert.Equal("150", total, "total for %s", pt)
		if pt == entity.PaymentType_WireTransfer {
			assert.Equal("150", wire, "wire updates for WireTransfer")
		} else {
			assert.Equal("100", wire, "wire unchanged for %s", pt)
		}
		assert.Equal("", market, "marketplace stays empty for %s", pt)
	}
}

// TestApplySupportDelta_EmptyWireOrMarketplacePassThrough: the
// accumulators may be empty (no commitment, or non-applicable payment
// type). The helper must pass them through unchanged instead of
// failing or treating "" as 0 (which would silently drop the column).
func TestApplySupportDelta_EmptyWireOrMarketplacePassThrough(t *testing.T) {
	assert := assert.New(t)

	inv, total, wire, market := applySupportDelta(
		"100", "100", "", "",
		decimal.RequireFromString("50"),
		entity.PaymentType_WireTransfer,
		true,
	)
	assert.Equal("150", inv)
	assert.Equal("150", total)
	assert.Equal("", wire, "empty wire passes through")
	assert.Equal("", market, "empty marketplace passes through")
}

// TestApplySupportDelta_ZeroDelta: defensively confirm zero delta is a
// no-op (the caller filters this out, but the helper should still
// produce the right answer).
func TestApplySupportDelta_ZeroDelta(t *testing.T) {
	assert := assert.New(t)

	inv, total, wire, market := applySupportDelta(
		"100", "100", "100", "",
		decimal.Zero,
		entity.PaymentType_WireTransfer,
		true,
	)
	assert.Equal("100", inv)
	assert.Equal("100", total)
	assert.Equal("100", wire)
	assert.Equal("", market, "WireTransfer doesn't touch marketplace")
}

// TestPlanActiveWindow verifies the helper that computes the plan's
// active window intersected with the calendar month. (Pure function —
// no DB needed.)
func TestPlanActiveWindow(t *testing.T) {
	assert := assert.New(t)

	plan := &entity.BindedSupportPlan{
		ActiveDate: time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC),
	}
	monthStart := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
	monthEnd := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)

	planStart, planEnd, activeDays := planActiveWindow(plan, monthStart, monthEnd)
	assert.Equal(time.Month(6), planStart.Month())
	assert.Equal(15, planStart.Day())
	assert.Equal(time.Month(6), planEnd.Month())
	assert.Equal(30, planEnd.Day()) // last day of June
	assert.Equal(16, activeDays)   // 15..30 inclusive

	// Plan with ExpiredDate before monthEnd.
	expired := time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC)
	plan2 := &entity.BindedSupportPlan{
		ActiveDate:  time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC),
		ExpiredDate: &expired,
	}
	_, planEnd, activeDays = planActiveWindow(plan2, monthStart, monthEnd)
	assert.Equal(20, planEnd.Day())
	assert.Equal(6, activeDays) // 15..20 inclusive

	// Plan entirely outside the month.
	plan3 := &entity.BindedSupportPlan{
		ActiveDate: time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC),
	}
	_, _, activeDays = planActiveWindow(plan3, monthStart, monthEnd)
	assert.Equal(0, activeDays)
}
