package tests

import (
	"net/http"
	"strings"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/payment/reconciliation"
	"github.com/pingcap/dbaas/billing2/utils/panicIf"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/shopspring/decimal"
	"github.com/stripe/stripe-go/v76"
)

// setupStripeARFixtures seeds two external tenants with a direct billing
// config, an active Stripe payment method and one monthly GENERATED invoice
// for datainit.BillingDate's month (2021-11):
//   - tenant 1: Stripe customer code "123"
//   - tenant 2: Stripe customer code "456"
//
// The Stripe mock (MockStripeHandler.GetMonthlyCreatedInvoices) only ever
// returns invoices for customer "123", so tenant 2 exercises the row-drop
// rule for Stripe tenants that never got pushed.
func (suite *ARTableTestSuite) setupStripeARFixtures() {
	MockPaymentHandler()
	central := &MockCentralHandlerImpl{}
	central.SetAllTenants([]forms.Tenant{
		{TenantID: 1, Kind: forms.TenantKindExternal},
		{TenantID: 2, Kind: forms.TenantKindExternal},
	})
	centralhandler.SetCentralHandler(central)

	for _, tenantID := range []uint{1, 2} {
		bcfg, err := datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
		panicIf.Error(err)
		paymentCode := "123"
		if tenantID == 2 {
			paymentCode = "456"
		}
		pm := &entity.PaymentMethod{
			PaymentType:           entity.PaymentType_STRIPE,
			Payable:               true,
			PaymentCode:           paymentCode,
			TenantID:              tenantID,
			Status:                entity.PaymentMethodStatus_Active,
			BindedBillingConfigID: bcfg.BillingConfigID,
		}
		suite.NoError(pm.Create(db.GetDB()))

		invoice := &entity.Invoice{
			InvoiceDate:           datainit.BillingDate,
			CostPrice:             decimal.NewFromInt(2),
			RawPrice:              decimal.NewFromInt(2),
			Type:                  entity.InvoiceType_MONTHLY,
			BindedBillingConfigID: bcfg.BillingConfigID,
			Status:                entity.InvoiceStatus_GENERATED,
			TenantID:              tenantID,
		}
		suite.NoError(invoice.Create(db.GetDB()))

		unitCost := &entity.UserUnitCost{
			CostID:                uuid.NewString(),
			CostTimeStamp:         datainit.BillingDate.Unix(),
			CostPrice:             decimal.NewFromInt(2),
			RawPrice:              decimal.NewFromInt(2),
			TenantID:              tenantID,
			Status:                entity.UserUnitCostStatus_DONE,
			ConsumedSource:        entity.ConsumedSource_Invoice,
			BindedBillingConfigID: bcfg.BillingConfigID,
			InvoiceID:             int(invoice.ID),
			Unit:                  datainit.AWSNodeUnit,
		}
		suite.NoError(unitCost.Create(db.GetDB()))
	}
}

// autoPushedStripeInvoice mimics an invoice created by the batch auto-push
// paying worker: it carries only merged_months metadata (no
// invoice_start/invoice_end), so its StartDate is zero. AR month attribution
// must fall back to the newest month in merged_months.
func autoPushedStripeInvoice(customerID, number, months string) *forms.StripeInvoice {
	return &forms.StripeInvoice{
		CustomerID: customerID,
		Status:     "open",
		Number:     number,
		SubTotal:   decimal.NewFromInt(2),
		Total:      decimal.NewFromInt(2),
		TaxRate:    decimal.Zero,
		InvoicePDF: number + ".pdf",
		Paid:       true,
		Created:    datainit.BillingDate.AddDate(0, 0, 2).Unix(),
		Metadata: map[string]string{
			paymenthandler.PushMetadataMergedMonths: months,
		},
		StatusTransitions: &stripe.InvoiceStatusTransitions{
			FinalizedAt: datainit.BillingDate.AddDate(0, 0, 2).Unix(),
			PaidAt:      datainit.BillingDate.AddDate(0, 0, 2).Unix(),
		},
	}
}

// csvRow returns the CSV line starting with the given prefix, or "".
func csvRow(csv, prefix string) string {
	for _, line := range strings.Split(csv, "\n") {
		if strings.HasPrefix(line, prefix) {
			return line
		}
	}
	return ""
}

// TestARTableActualIncludesAutoPushedInvoiceByMergedMonths drives the actual
// AR generation (GET /cr_tables/init, the same GenerateARInitTable +
// SaveARTableToDB + RenderARTable path the ARActualWorker runs) with a Stripe
// invoice that only carries merged_months = "2021-11". The tenant must end up
// in the AR table with the pushed invoice's stripe columns populated.
func (suite *ARTableTestSuite) TestARTableActualIncludesAutoPushedInvoiceByMergedMonths() {
	suite.setupStripeARFixtures()
	arTableMockStripeMonthlyInvoices = []*forms.StripeInvoice{
		autoPushedStripeInvoice("123", "INV-AUTO-202111", "2021-11"),
	}
	defer func() { arTableMockStripeMonthlyInvoices = nil }()

	w := tests.PerformRequest(suite.router, "GET", "/cr_tables/init?year=2021&month=11&day=1", nil)
	suite.Equal(http.StatusOK, w.Code, w.Body.String())
	body := w.Body.String()

	// Tenant 1 was auto-pushed (merged_months == the generated month): row
	// present with the stripe invoice number / pdf filled in.
	row1 := csvRow(body, "2021-11,2021-11-01,2021-11-30,1,")
	suite.NotEmpty(row1, "tenant 1 row missing:\n%s", body)
	suite.Contains(row1, "INV-AUTO-202111")
	suite.Contains(row1, "INV-AUTO-202111.pdf")

	// Tenant 2 has a local invoice but no Stripe invoice and no prepayment
	// credits: the STRIPE row must be dropped from the actual table.
	suite.Empty(csvRow(body, "2021-11,2021-11-01,2021-11-30,2,"), "tenant 2 should be dropped:\n%s", body)
}

// TestARTableEstimatedIncludesAutoPushedInvoiceByMergedMonths drives the
// estimated AR generation for the same scenario. Estimated rows are never
// dropped; the pushed invoice must fill the stripe columns of tenant 1 while
// tenant 2 keeps empty stripe columns.
func (suite *ARTableTestSuite) TestARTableEstimatedIncludesAutoPushedInvoiceByMergedMonths() {
	suite.setupStripeARFixtures()
	arTableMockStripeMonthlyInvoices = []*forms.StripeInvoice{
		autoPushedStripeInvoice("123", "INV-EST-202111", "2021-11"),
	}
	defer func() { arTableMockStripeMonthlyInvoices = nil }()

	rows, err := (&reconciliation.ARTableModel{}).GenerateEstimatedARTable(datainit.BillingDate)
	suite.NoError(err)

	var tenant1, tenant2 *reconciliation.ARTableRow
	for _, r := range rows {
		switch r.TenantID {
		case "1":
			tenant1 = r
		case "2":
			tenant2 = r
		}
	}
	suite.NotNil(tenant1, "tenant 1 row missing")
	suite.NotNil(tenant2, "tenant 2 row missing in estimated table (estimated rows are kept)")
	suite.Equal([]string{"INV-EST-202111"}, tenant1.StripeInvoiceNumber)
	suite.Equal([]string{"INV-EST-202111.pdf"}, tenant1.StripeInvoice)
	suite.Len(tenant2.StripeInvoice, 0)
}
