package pricing

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

type fakeStore struct {
	products map[int64]db.GetPricingProductRow
	units    map[int64]db.GetPricingProductUnitRow
}

func newFakeStore() *fakeStore {
	return &fakeStore{
		products: map[int64]db.GetPricingProductRow{
			1: {ID: 1, Code: "AIR-001", Name: "Air Mineral", SellingPrice: decimal.NewFromInt(12000), IsActive: true, UnitName: "Pcs", UnitSymbol: "pcs"},
			2: {ID: 2, Code: "OFF-001", Name: "Produk Nonaktif", SellingPrice: decimal.NewFromInt(5000), IsActive: false},
			3: {ID: 3, Code: "BARE-001", Name: "Tanpa Unit Config", SellingPrice: decimal.NewFromInt(7500), IsActive: true, UnitName: "Pcs", UnitSymbol: "pcs"},
		},
		units: map[int64]db.GetPricingProductUnitRow{},
	}
}

func (f *fakeStore) withUnit(u db.GetPricingProductUnitRow) *fakeStore {
	f.units[u.ID] = u
	return f
}

func (f *fakeStore) PricingProduct(_ context.Context, _ db.DBTX, id int64) (db.GetPricingProductRow, error) {
	if p, ok := f.products[id]; ok {
		return p, nil
	}
	return db.GetPricingProductRow{}, ErrProductNotFound
}

func (f *fakeStore) PricingProductUnit(_ context.Context, _ db.DBTX, id int64) (db.GetPricingProductUnitRow, error) {
	if u, ok := f.units[id]; ok {
		return u, nil
	}
	return db.GetPricingProductUnitRow{}, ErrUnitNotFound
}

func (f *fakeStore) PricingBaseUnit(_ context.Context, _ db.DBTX, productID int64) (db.GetPricingBaseUnitRow, error) {
	for _, u := range f.units {
		if u.ProductID == productID && u.IsBase && u.IsActive {
			return db.GetPricingBaseUnitRow(u), nil
		}
	}
	return db.GetPricingBaseUnitRow{}, ErrUnitNotFound
}

func nd(v int64) decimal.NullDecimal {
	return decimal.NullDecimal{Decimal: decimal.NewFromInt(v), Valid: true}
}

func baseUnit() db.GetPricingProductUnitRow {
	return db.GetPricingProductUnitRow{
		ID: 10, ProductID: 1, UnitID: 100,
		ConversionFactor: decimal.NewFromInt(1), MinPurchase: decimal.NewFromInt(1),
		IsBase: true, IsActive: true, UnitName: "Pcs", UnitSymbol: "pcs",
	}
}

func boxUnit() db.GetPricingProductUnitRow {
	return db.GetPricingProductUnitRow{
		ID: 11, ProductID: 1, UnitID: 101,
		ConversionFactor: decimal.NewFromInt(12), SellingPrice: nd(138000),
		MinPurchase: decimal.NewFromInt(2), IsActive: true,
		UnitName: "Box", UnitSymbol: "box",
	}
}

func TestResolve_ConfiguredUnitPriceWins(t *testing.T) {
	svc := NewService(newFakeStore().withUnit(baseUnit()).withUnit(boxUnit()))
	boxID := int64(11)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, ProductUnitID: &boxID, Quantity: decimal.NewFromInt(3)})
	if err != nil {
		t.Fatal(err)
	}
	if !got.UnitPrice.Equal(decimal.NewFromInt(138000)) {
		t.Errorf("unit_price = %s, want 138000", got.UnitPrice)
	}
	if !got.BaseQuantity.Equal(decimal.NewFromInt(36)) {
		t.Errorf("base_quantity = %s, want 36", got.BaseQuantity)
	}
	if !got.LineSubtotal.Equal(decimal.NewFromInt(414000)) {
		t.Errorf("line_subtotal = %s, want 414000", got.LineSubtotal)
	}
	if got.UnitName != "Box" || got.UnitSymbol != "box" {
		t.Errorf("unit = %q/%q, want Box/box", got.UnitName, got.UnitSymbol)
	}
}

func TestResolve_InheritsProductPriceScaledByFactor(t *testing.T) {
	unpriced := boxUnit()
	unpriced.SellingPrice = decimal.NullDecimal{}
	svc := NewService(newFakeStore().withUnit(baseUnit()).withUnit(unpriced))
	boxID := int64(11)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, ProductUnitID: &boxID, Quantity: decimal.NewFromInt(2)})
	if err != nil {
		t.Fatal(err)
	}
	// 12000 piece price x factor 12 = 144000 per box
	if !got.UnitPrice.Equal(decimal.NewFromInt(144000)) {
		t.Errorf("unit_price = %s, want 144000", got.UnitPrice)
	}
	if !got.BaseQuantity.Equal(decimal.NewFromInt(24)) {
		t.Errorf("base_quantity = %s, want 24", got.BaseQuantity)
	}
}

func TestResolve_DefaultsToBaseUnit(t *testing.T) {
	svc := NewService(newFakeStore().withUnit(baseUnit()).withUnit(boxUnit()))
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(5)})
	if err != nil {
		t.Fatal(err)
	}
	if got.ProductUnitID == nil || *got.ProductUnitID != 10 {
		t.Fatalf("product_unit_id = %v, want 10", got.ProductUnitID)
	}
	if !got.UnitPrice.Equal(decimal.NewFromInt(12000)) {
		t.Errorf("unit_price = %s, want 12000", got.UnitPrice)
	}
	if !got.BaseQuantity.Equal(decimal.NewFromInt(5)) {
		t.Errorf("base_quantity = %s, want 5", got.BaseQuantity)
	}
}

func TestResolve_ProductWithoutConfiguredUnits(t *testing.T) {
	svc := NewService(newFakeStore())
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 3, Quantity: decimal.NewFromInt(4)})
	if err != nil {
		t.Fatal(err)
	}
	if got.ProductUnitID != nil {
		t.Errorf("product_unit_id = %v, want nil", got.ProductUnitID)
	}
	if !got.ConversionFactor.Equal(decimal.NewFromInt(1)) {
		t.Errorf("conversion_factor = %s, want 1", got.ConversionFactor)
	}
	if !got.UnitPrice.Equal(decimal.NewFromInt(7500)) || !got.LineSubtotal.Equal(decimal.NewFromInt(30000)) {
		t.Errorf("price/subtotal = %s/%s, want 7500/30000", got.UnitPrice, got.LineSubtotal)
	}
}

func TestResolve_Rejections(t *testing.T) {
	foreign := boxUnit()
	foreign.ID = 12
	foreign.ProductID = 999

	inactive := boxUnit()
	inactive.ID = 13
	inactive.IsActive = false

	store := newFakeStore().withUnit(baseUnit()).withUnit(boxUnit()).withUnit(foreign).withUnit(inactive)
	svc := NewService(store)

	id := func(v int64) *int64 { return &v }
	tests := []struct {
		name string
		req  Request
		want error
	}{
		{"zero quantity", Request{ProductID: 1, Quantity: decimal.Zero}, ErrQuantityInvalid},
		{"negative quantity", Request{ProductID: 1, Quantity: decimal.NewFromInt(-1)}, ErrQuantityInvalid},
		{"unknown product", Request{ProductID: 404, Quantity: decimal.NewFromInt(1)}, ErrProductNotFound},
		{"inactive product", Request{ProductID: 2, Quantity: decimal.NewFromInt(1)}, ErrProductInactive},
		{"unknown unit", Request{ProductID: 1, ProductUnitID: id(404), Quantity: decimal.NewFromInt(1)}, ErrUnitNotFound},
		{"foreign unit", Request{ProductID: 1, ProductUnitID: id(12), Quantity: decimal.NewFromInt(2)}, ErrUnitNotForProduct},
		{"inactive unit", Request{ProductID: 1, ProductUnitID: id(13), Quantity: decimal.NewFromInt(2)}, ErrUnitInactive},
		{"below min purchase", Request{ProductID: 1, ProductUnitID: id(11), Quantity: decimal.NewFromInt(1)}, ErrBelowMinPurchase},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := svc.Resolve(context.Background(), nil, tc.req); !errors.Is(err, tc.want) {
				t.Fatalf("got %v, want %v", err, tc.want)
			}
		})
	}
}

func TestResolveAll_FailsWholeBatch(t *testing.T) {
	svc := NewService(newFakeStore().withUnit(baseUnit()).withUnit(boxUnit()))
	boxID := int64(11)
	_, err := svc.ResolveAll(context.Background(), nil, []Request{
		{ProductID: 1, Quantity: decimal.NewFromInt(1)},
		{ProductID: 1, ProductUnitID: &boxID, Quantity: decimal.NewFromInt(1)},
	})
	if !errors.Is(err, ErrBelowMinPurchase) {
		t.Fatalf("got %v, want ErrBelowMinPurchase", err)
	}
	if err == nil || !strings.Contains(err.Error(), "line 2") {
		t.Fatalf("error %v should name the failing line", err)
	}
}

func TestResolveAll_FractionalQuantity(t *testing.T) {
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	half, err := decimal.NewFromString("2.5")
	if err != nil {
		t.Fatal(err)
	}
	got, err := svc.ResolveAll(context.Background(), nil, []Request{{ProductID: 1, Quantity: half}})
	if err != nil {
		t.Fatal(err)
	}
	if !got[0].BaseQuantity.Equal(half) {
		t.Errorf("base_quantity = %s, want 2.5", got[0].BaseQuantity)
	}
	if !got[0].LineSubtotal.Equal(decimal.NewFromInt(30000)) {
		t.Errorf("line_subtotal = %s, want 30000", got[0].LineSubtotal)
	}
}

// --- Phase 6 item 3: membership benefit (§13.1) -------------------------------

func TestTierDiscountPercent(t *testing.T) {
	cases := []struct {
		tier string
		want string
	}{
		{"bronze", "2"},
		{"gold", "5"},
		{"platinum", "8"},
		{"BRONZE", "2"},
	}
	for _, tc := range cases {
		pct, err := TierDiscountPercent(tc.tier)
		if err != nil {
			t.Fatalf("%s: %v", tc.tier, err)
		}
		if !pct.Equal(decimal.RequireFromString(tc.want)) {
			t.Errorf("tier %s percent = %s, want %s", tc.tier, pct, tc.want)
		}
	}
	if _, err := TierDiscountPercent("silver"); !errors.Is(err, ErrUnknownTier) {
		t.Errorf("silver should be ErrUnknownTier, got %v", err)
	}
	if _, err := TierDiscountPercent("bogus"); !errors.Is(err, ErrUnknownTier) {
		t.Errorf("bogus should be ErrUnknownTier, got %v", err)
	}
}

// membershipResolver returns a fixed tier/ok for any customer id.
func membershipResolver(calls *int, tier string, ok bool, retErr error) MembershipLookup {
	return func(_ context.Context, _ db.DBTX, customerID int64) (string, bool, error) {
		*calls++
		_ = customerID
		return tier, ok, retErr
	}
}

// Bug sweep R3-1: CustomerID 0 is the walk-in sentinel. The sales service maps
// it to the "Pelanggan Umum" placeholder for persistence; a stray membership
// row on that placeholder must never silently discount every walk-in sale.
// The lookup is skipped entirely and the line prices at list.
func TestResolve_WalkInSentinelZeroSkipsMembership(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "bronze", true, nil))
	walkIn := int64(0)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &walkIn})
	if err != nil {
		t.Fatal(err)
	}
	if calls != 0 {
		t.Errorf("resolver calls = %d, want 0 (walk-in sentinel must skip the lookup)", calls)
	}
	if got.MembershipTier != "" || !got.MembershipDiscountAmount.IsZero() {
		t.Errorf("walk-in picked up a benefit: tier=%q amount=%s", got.MembershipTier, got.MembershipDiscountAmount)
	}
	if !got.FinalSubtotal.Equal(got.LineSubtotal) {
		t.Errorf("final_subtotal = %s, want line_subtotal %s (list price)", got.FinalSubtotal, got.LineSubtotal)
	}
}

// A nil CustomerID keeps its existing contract: no membership stage at all.
func TestResolve_NilCustomerSkipsMembership(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "bronze", true, nil))
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1)})
	if err != nil {
		t.Fatal(err)
	}
	if calls != 0 {
		t.Errorf("resolver calls = %d, want 0 (nil customer means no lookup)", calls)
	}
	if !got.FinalSubtotal.Equal(got.LineSubtotal) {
		t.Errorf("final_subtotal = %s, want line_subtotal %s", got.FinalSubtotal, got.LineSubtotal)
	}
}

func TestResolve_MembershipBronzeBenefit(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "bronze", true, nil))
	cust := int64(99)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if err != nil {
		t.Fatal(err)
	}
	// 12000 * 0.02 = 240; FinalSubtotal = 11760
	if !got.MembershipDiscountAmount.Equal(decimal.NewFromInt(240)) {
		t.Errorf("discount_amount = %s, want 240", got.MembershipDiscountAmount)
	}
	if !got.MembershipDiscountPercent.Equal(decimal.NewFromInt(2)) {
		t.Errorf("discount_percent = %s, want 2", got.MembershipDiscountPercent)
	}
	if got.MembershipTier != "bronze" {
		t.Errorf("tier = %q, want bronze", got.MembershipTier)
	}
	if !got.FinalSubtotal.Equal(decimal.NewFromInt(11760)) {
		t.Errorf("final_subtotal = %s, want 11760", got.FinalSubtotal)
	}
	if !got.LineSubtotal.Equal(decimal.NewFromInt(12000)) {
		t.Errorf("line_subtotal = %s, want 12000", got.LineSubtotal)
	}
	if calls != 1 {
		t.Errorf("resolver calls = %d, want 1", calls)
	}
}

func TestResolve_MembershipGoldBenefit(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "gold", true, nil))
	cust := int64(99)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if err != nil {
		t.Fatal(err)
	}
	// 12000 * 0.05 = 600; FinalSubtotal = 11400
	if !got.MembershipDiscountAmount.Equal(decimal.NewFromInt(600)) {
		t.Errorf("discount_amount = %s, want 600", got.MembershipDiscountAmount)
	}
	if !got.FinalSubtotal.Equal(decimal.NewFromInt(11400)) {
		t.Errorf("final_subtotal = %s, want 11400", got.FinalSubtotal)
	}
}

func TestResolve_MembershipPlatinumBenefit(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "platinum", true, nil))
	cust := int64(99)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if err != nil {
		t.Fatal(err)
	}
	// 12000 * 0.08 = 960; FinalSubtotal = 11040
	if !got.MembershipDiscountAmount.Equal(decimal.NewFromInt(960)) {
		t.Errorf("discount_amount = %s, want 960", got.MembershipDiscountAmount)
	}
	if !got.FinalSubtotal.Equal(decimal.NewFromInt(11040)) {
		t.Errorf("final_subtotal = %s, want 11040", got.FinalSubtotal)
	}
}

func TestResolve_MembershipInactiveNoBenefit(t *testing.T) {
	var calls int
	// ok=false => no active membership; line untouched.
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "", false, nil))
	cust := int64(99)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if err != nil {
		t.Fatal(err)
	}
	if !got.LineSubtotal.Equal(decimal.NewFromInt(12000)) {
		t.Errorf("line_subtotal = %s, want 12000 (unchanged)", got.LineSubtotal)
	}
	if calls != 1 {
		t.Errorf("resolver calls = %d, want 1", calls)
	}
	if got.MembershipTier != "" {
		t.Errorf("tier = %q, want empty", got.MembershipTier)
	}
	if !got.MembershipDiscountAmount.IsZero() {
		t.Errorf("discount_amount = %s, want 0", got.MembershipDiscountAmount)
	}
	if !got.FinalSubtotal.Equal(decimal.NewFromInt(12000)) {
		t.Errorf("final_subtotal = %s, want 12000", got.FinalSubtotal)
	}
}

func TestResolve_MembershipUnknownTierError(t *testing.T) {
	var calls int
	// An active membership with an unrecognized tier must fail closed.
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "silver", true, nil))
	cust := int64(99)
	_, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if !errors.Is(err, ErrUnknownTier) {
		t.Fatalf("got %v, want ErrUnknownTier", err)
	}
	if calls != 1 {
		t.Errorf("resolver calls = %d, want 1", calls)
	}
}

func TestResolve_NoCustomerNoBenefit(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "bronze", true, nil))
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1)})
	if err != nil {
		t.Fatal(err)
	}
	if !got.LineSubtotal.Equal(decimal.NewFromInt(12000)) {
		t.Errorf("line_subtotal = %s, want 12000", got.LineSubtotal)
	}
	if calls != 0 {
		t.Errorf("resolver should not be called when no customer, got %d", calls)
	}
}

func TestResolveAll_MembershipBenefitOncePerLine(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "bronze", true, nil))
	cust := int64(99)
	got, err := svc.ResolveAll(context.Background(), nil, []Request{
		{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust},
		{ProductID: 3, Quantity: decimal.NewFromInt(1), CustomerID: &cust},
	})
	if err != nil {
		t.Fatal(err)
	}
	if calls != 2 {
		t.Fatalf("resolver calls = %d, want 2 (once per line)", calls)
	}
	// 12000 -> 11760 ; 7500 -> 7350
	if !got[0].FinalSubtotal.Equal(decimal.NewFromInt(11760)) {
		t.Errorf("line0 final = %s, want 11760", got[0].FinalSubtotal)
	}
	if !got[1].FinalSubtotal.Equal(decimal.NewFromInt(7350)) {
		t.Errorf("line1 final = %s, want 7350", got[1].FinalSubtotal)
	}
	// Deterministic: a second call yields identical results.
	again, err := svc.ResolveAll(context.Background(), nil, []Request{
		{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust},
		{ProductID: 3, Quantity: decimal.NewFromInt(1), CustomerID: &cust},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !got[0].FinalSubtotal.Equal(again[0].FinalSubtotal) || !got[1].FinalSubtotal.Equal(again[1].FinalSubtotal) {
		t.Errorf("ResolveAll not deterministic: %s/%s vs %s/%s",
			got[0].FinalSubtotal, got[1].FinalSubtotal, again[0].FinalSubtotal, again[1].FinalSubtotal)
	}
}

// TestResolve_TierResolverOverride proves the DB-backed resolver path is used
// when wired: a custom 10% overrides the gold 5% fallback map.
func TestResolve_TierResolverOverride(t *testing.T) {
	var calls int
	svc := NewService(newFakeStore().withUnit(baseUnit()))
	svc.WithMembershipLookup(membershipResolver(&calls, "gold", true, nil))
	svc.WithTierBenefitResolver(func(_ context.Context, _ db.DBTX, _ string) (decimal.Decimal, error) {
		return decimal.NewFromInt(10), nil
	})
	cust := int64(99)
	got, err := svc.Resolve(context.Background(), nil, Request{ProductID: 1, Quantity: decimal.NewFromInt(1), CustomerID: &cust})
	if err != nil {
		t.Fatal(err)
	}
	// 12000 * 0.10 = 1200; FinalSubtotal = 10800
	if !got.MembershipDiscountPercent.Equal(decimal.NewFromInt(10)) {
		t.Errorf("discount_percent = %s, want 10", got.MembershipDiscountPercent)
	}
	if !got.MembershipDiscountAmount.Equal(decimal.NewFromInt(1200)) {
		t.Errorf("discount_amount = %s, want 1200", got.MembershipDiscountAmount)
	}
	if !got.FinalSubtotal.Equal(decimal.NewFromInt(10800)) {
		t.Errorf("final_subtotal = %s, want 10800", got.FinalSubtotal)
	}
	if got.MembershipTier != "gold" {
		t.Errorf("tier = %q, want gold", got.MembershipTier)
	}
}
