package partners

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"
)

type customerLogin struct {
	ID       int64
	IsActive bool
}

func testLoc() *time.Location { return time.UTC }

func customerRow(id int64) db.GetCustomerByIDRow {
	return db.GetCustomerByIDRow{ID: id, Code: fmt.Sprintf("CUS-%03d", id), Name: "Customer Test"}
}

type fakeStore struct {
	suppliers      map[int64]db.GetSupplierByIDRow
	customers      map[int64]db.GetCustomerByIDRow
	customerUsers  map[int64]customerLogin
	passwords      map[int64]string
	supplierFilter ListFilter
	customerFilter ListFilter
	nextID         int64
}

func newFakeStore() *fakeStore {
	return &fakeStore{
		suppliers:     map[int64]db.GetSupplierByIDRow{},
		customers:     map[int64]db.GetCustomerByIDRow{},
		customerUsers: map[int64]customerLogin{},
		passwords:     map[int64]string{},
		nextID:        1,
	}
}

func (f *fakeStore) ListSuppliers(_ context.Context, filter ListFilter) ([]db.ListSuppliersRow, int, error) {
	f.supplierFilter = filter
	return nil, 0, nil
}
func (f *fakeStore) SupplierByID(_ context.Context, id int64) (db.GetSupplierByIDRow, error) {
	if s, ok := f.suppliers[id]; ok {
		return s, nil
	}
	return db.GetSupplierByIDRow{}, ErrNotFound
}
func (f *fakeStore) CreateSupplier(_ context.Context, p db.CreateSupplierParams) (int64, error) {
	id := f.nextID
	f.nextID++
	f.suppliers[id] = db.GetSupplierByIDRow{
		ID:            id,
		Code:          p.Code,
		Name:          p.Name,
		ContactPerson: p.ContactPerson,
		Phone:         p.Phone,
		Email:         p.Email,
		Address:       p.Address,
		City:          p.City,
		TaxNumber:     p.TaxNumber,
		BankName:      p.BankName,
		BankAccount:   p.BankAccount,
		CreditLimit:   p.CreditLimit,
		PaymentTerms:  p.PaymentTerms,
		IsActive:      p.IsActive,
	}
	return id, nil
}
func (f *fakeStore) UpdateSupplier(_ context.Context, p db.UpdateSupplierParams) error {
	prev, ok := f.suppliers[p.ID]
	if !ok {
		return ErrNotFound
	}
	prev.Name = p.Name
	prev.ContactPerson = p.ContactPerson
	prev.Phone = p.Phone
	prev.Email = p.Email
	prev.Address = p.Address
	prev.City = p.City
	prev.TaxNumber = p.TaxNumber
	prev.BankName = p.BankName
	prev.BankAccount = p.BankAccount
	prev.CreditLimit = p.CreditLimit
	prev.PaymentTerms = p.PaymentTerms
	prev.IsActive = p.IsActive
	f.suppliers[p.ID] = prev
	return nil
}
func (f *fakeStore) DeleteSupplier(context.Context, int64) error { return nil }
func (f *fakeStore) SupplierByCode(_ context.Context, code string) (db.GetSupplierByCodeRow, error) {
	for _, s := range f.suppliers {
		if s.Code == code {
			return db.GetSupplierByCodeRow{ID: s.ID, Code: s.Code, Name: s.Name}, nil
		}
	}
	return db.GetSupplierByCodeRow{}, ErrNotFound
}
func (f *fakeStore) ListCustomers(_ context.Context, filter ListFilter) ([]db.ListCustomersRow, int, error) {
	f.customerFilter = filter
	return nil, 0, nil
}
func (f *fakeStore) CustomerByID(_ context.Context, id int64) (db.GetCustomerByIDRow, error) {
	if c, ok := f.customers[id]; ok {
		return c, nil
	}
	return db.GetCustomerByIDRow{}, ErrNotFound
}
func (f *fakeStore) CustomerByCode(_ context.Context, code string) (db.GetCustomerByCodeRow, error) {
	for _, c := range f.customers {
		if c.Code == code {
			return db.GetCustomerByCodeRow{ID: c.ID, Code: c.Code, Name: c.Name}, nil
		}
	}
	return db.GetCustomerByCodeRow{}, ErrNotFound
}
func (f *fakeStore) CreateCustomer(_ context.Context, p db.CreateCustomerParams) (int64, error) {
	id := f.nextID
	f.nextID++
	f.customers[id] = db.GetCustomerByIDRow{
		ID:              id,
		Code:            p.Code,
		Name:            p.Name,
		ContactPerson:   p.ContactPerson,
		Phone:           p.Phone,
		Email:           p.Email,
		Address:         p.Address,
		City:            p.City,
		TaxNumber:       p.TaxNumber,
		CreditLimit:     p.CreditLimit,
		PaymentTerms:    p.PaymentTerms,
		CustomerType:    p.CustomerType,
		AssignedSalesID: p.AssignedSalesID,
		IsActive:        p.IsActive,
	}
	return id, nil
}
func (f *fakeStore) UpdateCustomer(_ context.Context, p db.UpdateCustomerParams) error {
	prev, ok := f.customers[p.ID]
	if !ok {
		return ErrNotFound
	}
	prev.Name = p.Name
	prev.ContactPerson = p.ContactPerson
	prev.Phone = p.Phone
	prev.Email = p.Email
	prev.Address = p.Address
	prev.City = p.City
	prev.TaxNumber = p.TaxNumber
	prev.CreditLimit = p.CreditLimit
	prev.PaymentTerms = p.PaymentTerms
	prev.CustomerType = p.CustomerType
	prev.AssignedSalesID = p.AssignedSalesID
	prev.IsActive = p.IsActive
	f.customers[p.ID] = prev
	return nil
}
func (f *fakeStore) DeleteCustomer(context.Context, int64) error { return nil }
func (f *fakeStore) CustomerLoginUser(_ context.Context, id int64) (db.GetCustomerLoginUserRow, error) {
	if u, ok := f.customerUsers[id]; ok {
		return db.GetCustomerLoginUserRow{ID: u.ID, IsActive: u.IsActive}, nil
	}
	return db.GetCustomerLoginUserRow{}, pgx.ErrNoRows
}
func (f *fakeStore) SetUserPassword(_ context.Context, userID int64, passwordHash string) error {
	f.passwords[userID] = passwordHash
	return nil
}

// Profile state for the /customers/me tests: which user owns which customer,
// and what the last update wrote.
var (
	fakeProfileByUser = map[int64]db.CustomerProfileForUserRow{}
	fakeProfileUpdate db.UpdateCustomerProfileForUserParams
)

func (f *fakeStore) CustomerProfileForUser(_ context.Context, userID int64) (db.CustomerProfileForUserRow, error) {
	row, ok := fakeProfileByUser[userID]
	if !ok {
		return db.CustomerProfileForUserRow{}, pgx.ErrNoRows
	}
	return row, nil
}

func (f *fakeStore) UpdateCustomerProfileForUser(_ context.Context, p db.UpdateCustomerProfileForUserParams) (int64, error) {
	fakeProfileUpdate = p
	return 1, nil
}

type fakeSeq struct{ n int64 }

func (f *fakeSeq) Sequence(context.Context, string, time.Time, any) (int64, error) {
	f.n++
	return f.n, nil
}

func TestCreateSupplier_Code(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	s, err := svc.CreateSupplier(context.Background(), SupplierInput{
		Name:         "PT Maju",
		PaymentTerms: 30,
	})
	if err != nil {
		t.Fatal(err)
	}
	if s.Code != "SUP-001" {
		t.Errorf("code = %q, want SUP-001", s.Code)
	}
	if s.PaymentTerms != 30 {
		t.Errorf("payment_terms = %d, want 30", s.PaymentTerms)
	}
}

func TestCreateSupplier_EmptyName(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	if _, err := svc.CreateSupplier(context.Background(), SupplierInput{}); err == nil {
		t.Fatal("want error for empty name")
	}
}

func TestCreateCustomer_CodeAndType(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "Toko ABC"})
	if err != nil {
		t.Fatal(err)
	}
	if c.Code != "CUS-001" {
		t.Errorf("code = %q, want CUS-001", c.Code)
	}
	if c.CustomerType != "retail" {
		t.Errorf("default customer_type = %q, want retail", c.CustomerType)
	}
}

func TestCreateCustomer_InvalidType(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	_, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "X", CustomerType: "alien"})
	if err == nil {
		t.Fatal("want error for invalid customer_type")
	}
}

func TestCreateCustomer_RoundMoney(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{
		Name:        "Toko XYZ",
		CreditLimit: decimal.NewFromFloat(1000.129),
	})
	if err != nil {
		t.Fatal(err)
	}
	if got := c.CreditLimit.String(); got != "1000.13" {
		t.Errorf("credit_limit = %s, want 1000.13", got)
	}
}

func TestUpdateCustomer_KeepsTypeWhenOmitted(t *testing.T) {
	st := newFakeStore()
	svc := NewService(st, &fakeSeq{}, time.UTC)
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "T", CustomerType: "wholesale"})
	if err != nil {
		t.Fatal(err)
	}
	updated, err := svc.UpdateCustomer(context.Background(), c.ID, CustomerInput{Name: "T2"})
	if err != nil {
		t.Fatal(err)
	}
	if updated.CustomerType != "wholesale" {
		t.Errorf("customer_type = %q, want wholesale", updated.CustomerType)
	}
}

func TestUpdateCustomerDeactivationFiresHook(t *testing.T) {
	// R2-6a: flipping a customer inactive must notify the login-sync hook
	// so the linked login loses access too.
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	var fired []int64
	svc.WithOnCustomerDeactivated(func(_ context.Context, customerID int64) error {
		fired = append(fired, customerID)
		return nil
	})
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "Toko A", IsActive: true})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := svc.UpdateCustomer(context.Background(), c.ID, CustomerInput{Name: "Toko A", IsActive: false}); err != nil {
		t.Fatal(err)
	}
	if len(fired) != 1 || fired[0] != c.ID {
		t.Fatalf("hook = %v, want [%d]", fired, c.ID)
	}
}

func TestUpdateCustomerWithoutDeactivationFiresNothing(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	fired := 0
	svc.WithOnCustomerDeactivated(func(_ context.Context, _ int64) error {
		fired++
		return nil
	})
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "Toko B", IsActive: true})
	if err != nil {
		t.Fatal(err)
	}
	// Plain edit on an active customer: no login impact.
	if _, err := svc.UpdateCustomer(context.Background(), c.ID, CustomerInput{Name: "Toko B2", IsActive: true}); err != nil {
		t.Fatal(err)
	}
	if fired != 0 {
		t.Fatalf("hook fired %d times, want 0", fired)
	}
}

func TestDeleteCustomerFiresHook(t *testing.T) {
	svc := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	var fired []int64
	svc.WithOnCustomerDeactivated(func(_ context.Context, customerID int64) error {
		fired = append(fired, customerID)
		return nil
	})
	c, err := svc.CreateCustomer(context.Background(), CustomerInput{Name: "Toko C", IsActive: true})
	if err != nil {
		t.Fatal(err)
	}
	if err := svc.DeleteCustomer(context.Background(), c.ID); err != nil {
		t.Fatal(err)
	}
	if len(fired) != 1 || fired[0] != c.ID {
		t.Fatalf("hook = %v, want [%d]", fired, c.ID)
	}
}

func TestListFiltersForwarded(t *testing.T) {
	st := newFakeStore()
	svc := NewService(st, &fakeSeq{}, time.UTC)

	if _, _, err := svc.ListSuppliers(context.Background(), ListFilter{Search: "maju", Status: "active"}); err != nil {
		t.Fatal(err)
	}
	if st.supplierFilter.Page != 1 || st.supplierFilter.PerPage != 20 || st.supplierFilter.Search != "maju" || st.supplierFilter.Status != "active" {
		t.Errorf("supplier filter = %+v", st.supplierFilter)
	}

	if _, _, err := svc.ListCustomers(context.Background(), ListFilter{Page: 2, PerPage: 50, Search: "toko", Status: "inactive", CustomerType: "retail"}); err != nil {
		t.Fatal(err)
	}
	if st.customerFilter != (ListFilter{Page: 2, PerPage: 50, Search: "toko", Status: "inactive", CustomerType: "retail"}) {
		t.Errorf("customer filter = %+v", st.customerFilter)
	}
}

func TestSupplierView_NullableToEmpty(t *testing.T) {
	v := supplierView(db.GetSupplierByIDRow{ID: 1, Code: "SUP-001", Name: "X", PaymentTerms: 30})
	if v.ContactPerson != "" || v.Phone != "" || v.Email != "" {
		t.Errorf("nullable fields should default to empty string: %+v", v)
	}
	if v.DeletedAt != nil {
		t.Errorf("deleted_at should be nil: %+v", v.DeletedAt)
	}
}

func TestCustomerView_AssignedSalesID(t *testing.T) {
	v := customerView(db.GetCustomerByIDRow{ID: 1, Code: "CUS-001", Name: "X",
		AssignedSalesID: pgtype.Int8{Int64: 5, Valid: true}})
	if v.AssignedSalesID == nil || *v.AssignedSalesID != 5 {
		t.Errorf("assigned_sales_id = %v, want 5", v.AssignedSalesID)
	}
}

// R2-5a. The storefront account page reads six fields; customers had columns
// for three, so the repos stubbed getCustomer as `async () => null` and the page
// rendered a form that could not be saved in live mode.
//
// MyProfile is keyed by users.id, never by an id in the path. The company
// directory sits behind an internal PII gate precisely so a storefront buyer
// cannot enumerate it; a get-by-id profile route would undo that while looking
// like a convenience.
func TestMyProfileResolvesFromTheTokenNotAnID(t *testing.T) {
	fakeProfileByUser = map[int64]db.CustomerProfileForUserRow{
		42: {ID: 7, Code: "CUS-001", Name: "Toko SAYA"},
		43: {ID: 9, Code: "CUS-002", Name: "Toko LAIN"},
	}
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)

	me, err := s.MyProfile(context.Background(), 42)
	if err != nil {
		t.Fatal(err)
	}
	if me.ID != 7 || me.Name != "Toko SAYA" {
		t.Errorf("profile = %+v, want the caller's own record (id 7)", me)
	}
	other, err := s.MyProfile(context.Background(), 43)
	if err != nil {
		t.Fatal(err)
	}
	if other.ID == me.ID {
		t.Error("two users resolved to the same customer record — the lookup is not scoped to the caller")
	}
	// A user with no customer row is a 404, not someone else's record.
	if _, err := s.MyProfile(context.Background(), 999); !errors.Is(err, ErrNotFound) {
		t.Errorf("err = %v, want ErrNotFound — no record, never a neighbour's", err)
	}
}

// A nil field means "leave it alone"; a pointer to "" clears it.
//
// This is the test for the bug that shipped in the first version of
// UpdateMyProfile. It took CustomerInput, whose fields are plain strings, and
// mapped them with pgtext() — always valid. An omitted field therefore reached
// the UPDATE as an empty STRING rather than NULL, COALESCE picked it, and
// submitting {"website":""} blanked the customer's city, kecamatan, phone and
// everything else in the row. Same class as the settings price-list zeroing
// fixed in 8d570e1, and the same rule: a partial payload must never be able to
// write to a column it did not mention.
func TestUpdateMyProfileLeavesUnmentionedColumnsAlone(t *testing.T) {
	fakeProfileByUser = map[int64]db.CustomerProfileForUserRow{42: {ID: 7, Name: "Toko"}}
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)

	// Exactly the payload a profile form sends when one field is edited.
	empty := ""
	_, err := s.UpdateMyProfile(context.Background(), 42, ProfileInput{Website: &empty})
	if err != nil {
		t.Fatal(err)
	}

	if !fakeProfileUpdate.Website.Valid || fakeProfileUpdate.Website.String != "" {
		t.Errorf("website = %+v, want CLEARED (Valid with an empty string, not NULL)", fakeProfileUpdate.Website)
	}
	// The whole point: nothing else moved.
	for name, got := range map[string]pgtype.Text{
		"contact_person": fakeProfileUpdate.ContactPerson,
		"phone":          fakeProfileUpdate.Phone,
		"email":          fakeProfileUpdate.Email,
		"address":        fakeProfileUpdate.Address,
		"city":           fakeProfileUpdate.City,
		"kecamatan":      fakeProfileUpdate.Kecamatan,
		"kelurahan":      fakeProfileUpdate.Kelurahan,
		"desa":           fakeProfileUpdate.Desa,
		"tax_number":     fakeProfileUpdate.TaxNumber,
	} {
		if got.Valid {
			t.Errorf("%s = %+v, want NULL (invalid) so COALESCE keeps the stored value. "+
				"An omitted field must never write to its column.", name, got)
		}
	}
	if fakeProfileUpdate.Name.Valid {
		t.Errorf("name = %+v, want NULL (invalid) — the payload never mentioned it", fakeProfileUpdate.Name)
	}
	if fakeProfileUpdate.BillingAddressID.Valid || fakeProfileUpdate.ShippingAddressID.Valid {
		t.Errorf("address pointers = %+v / %+v, want NULL", fakeProfileUpdate.BillingAddressID, fakeProfileUpdate.ShippingAddressID)
	}
}

// A blank name is refused, not silently ignored. It is the one field with no
// useful empty state, and a 200 that changed nothing reads to the client as
// "this field is read-only".
func TestUpdateMyProfileRejectsBlankName(t *testing.T) {
	fakeProfileByUser = map[int64]db.CustomerProfileForUserRow{42: {ID: 7, Name: "Toko"}}
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	before := fakeProfileUpdate

	for _, blank := range []string{"", "   "} {
		if _, err := s.UpdateMyProfile(context.Background(), 42, ProfileInput{Name: &blank}); !errors.Is(err, ErrValidation) {
			t.Errorf("name=%q: err = %v, want ErrValidation", blank, err)
		}
	}
	if fakeProfileUpdate != before {
		t.Error("a rejected update still reached the store")
	}
}
