package warehousedeposit

import (
	"context"
	"testing"
	"time"

	"github.com/shopspring/decimal"
)

type fakePayment struct {
	amount    decimal.Decimal
	method    string
	depositID *int64
}

type fakeStore struct {
	deposits map[int64]*WarehouseDeposit
	nextID   int64
	drivers  map[int64]string
	payments map[int64]*fakePayment
}

func newFakeStore() *fakeStore {
	return &fakeStore{
		deposits: make(map[int64]*WarehouseDeposit),
		nextID:   1,
		drivers:  map[int64]string{1: "Pak Budi", 2: "Pak Joko"},
		payments: map[int64]*fakePayment{
			101: {amount: decimal.NewFromInt(250000), method: "cash"},
			102: {amount: decimal.NewFromInt(500000), method: "giro"},
			103: {amount: decimal.NewFromInt(100000), method: "transfer"}, // not eligible: wrong method
			104: {amount: decimal.NewFromInt(300000), method: "cash"},
		},
	}
}

func (f *fakeStore) DriverName(_ context.Context, driverID int64) (string, error) {
	name, ok := f.drivers[driverID]
	if !ok {
		return "", ErrNotFound
	}
	return name, nil
}

func (f *fakeStore) CreateWithPayments(_ context.Context, d *WarehouseDeposit, paymentIDs []int64) error {
	total := decimal.Zero
	for _, id := range paymentIDs {
		p, ok := f.payments[id]
		if !ok || (p.method != "cash" && p.method != "giro") || p.depositID != nil {
			return ErrPaymentInvalid
		}
		total = total.Add(p.amount)
	}
	d.ID = f.nextID
	f.nextID++
	now := time.Now()
	d.CreatedAt = now
	d.UpdatedAt = now
	d.Amount = total
	f.deposits[d.ID] = d
	for _, id := range paymentIDs {
		depID := d.ID
		f.payments[id].depositID = &depID
	}
	return nil
}

func (f *fakeStore) ListUndepositedPayments(_ context.Context, _ UndepositedFilter) ([]UndepositedPayment, error) {
	var items []UndepositedPayment
	for id, p := range f.payments {
		if p.depositID == nil && (p.method == "cash" || p.method == "giro") {
			items = append(items, UndepositedPayment{PaymentID: id, Amount: p.amount, PaymentMethod: p.method})
		}
	}
	return items, nil
}

func (f *fakeStore) GetByID(_ context.Context, id int64) (*WarehouseDeposit, error) {
	d, ok := f.deposits[id]
	if !ok {
		return nil, ErrNotFound
	}
	cp := *d
	return &cp, nil
}

func (f *fakeStore) GetByIDForUpdate(_ context.Context, id int64) (*WarehouseDeposit, error) {
	d, ok := f.deposits[id]
	if !ok {
		return nil, ErrNotFound
	}
	cp := *d
	return &cp, nil
}

func (f *fakeStore) List(_ context.Context, _ Filter) ([]WarehouseDeposit, int, error) {
	var list []WarehouseDeposit
	for _, d := range f.deposits {
		list = append(list, *d)
	}
	return list, len(list), nil
}

func (f *fakeStore) UpdateReceived(_ context.Context, id, receivedBy int64, receivedAt time.Time) error {
	d, ok := f.deposits[id]
	if !ok {
		return ErrNotFound
	}
	d.Status = "received"
	d.ReceivedBy = &receivedBy
	d.ReceivedAt = &receivedAt
	return nil
}

func TestCreateDeposit(t *testing.T) {
	store := newFakeStore()
	svc := NewService(store, nil, time.UTC)

	uid := int64(10)
	driverID := int64(1)

	// Valid create: amount is computed from the selected payments, not typed.
	d, err := svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		DriverID:    &driverID,
		PaymentIDs:  []int64{101, 102},
		Notes:       "Setoran DO-1234",
	}, &uid)
	if err != nil {
		t.Fatalf("expected success, got %v", err)
	}
	if d.ID == 0 {
		t.Error("expected non-zero ID")
	}
	if d.Status != "pending" {
		t.Errorf("expected pending, got %s", d.Status)
	}
	if d.DriverName != "Pak Budi" {
		t.Errorf("expected Pak Budi, got %s", d.DriverName)
	}
	if d.DriverID == nil || *d.DriverID != 1 {
		t.Errorf("expected driver ID 1, got %v", d.DriverID)
	}
	wantAmount := decimal.NewFromInt(750000) // 250000 + 500000
	if !d.Amount.Equal(wantAmount) {
		t.Errorf("expected amount computed from selected payments (%s), got %s", wantAmount, d.Amount)
	}

	// Ad-hoc deposit with no driver (e.g. Sales handing over a direct payment).
	d2, err := svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		PaymentIDs:  []int64{104},
	}, &uid)
	if err != nil {
		t.Fatalf("expected success for driverless deposit, got %v", err)
	}
	if d2.DriverID != nil {
		t.Errorf("expected nil driver ID, got %v", d2.DriverID)
	}

	// Validation errors
	_, err = svc.Create(context.Background(), CreateInput{
		WarehouseID: 0,
		PaymentIDs:  []int64{104},
	}, &uid)
	if err != ErrWarehouseRequired {
		t.Errorf("expected ErrWarehouseRequired, got %v", err)
	}

	_, err = svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
	}, &uid)
	if err != ErrPaymentsRequired {
		t.Errorf("expected ErrPaymentsRequired, got %v", err)
	}

	unknownDriver := int64(999)
	_, err = svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		DriverID:    &unknownDriver,
		PaymentIDs:  []int64{104},
	}, &uid)
	if err != ErrDriverRequired {
		t.Errorf("expected ErrDriverRequired for unknown driver, got %v", err)
	}

	// Payment already claimed by the first deposit above is rejected.
	_, err = svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		PaymentIDs:  []int64{101},
	}, &uid)
	if err != ErrPaymentInvalid {
		t.Errorf("expected ErrPaymentInvalid for already-deposited payment, got %v", err)
	}

	// Transfer-method payment is not eligible for a Setoran.
	_, err = svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		PaymentIDs:  []int64{103},
	}, &uid)
	if err != ErrPaymentInvalid {
		t.Errorf("expected ErrPaymentInvalid for non-cash/giro payment, got %v", err)
	}
}

func TestReceiveDeposit(t *testing.T) {
	store := newFakeStore()
	svc := NewService(store, nil, time.UTC)

	uid := int64(10)
	driverID := int64(2)
	d, err := svc.Create(context.Background(), CreateInput{
		WarehouseID: 1,
		DriverID:    &driverID,
		PaymentIDs:  []int64{102},
		Notes:       "COD Setoran",
	}, &uid)
	if err != nil {
		t.Fatal(err)
	}

	// Kasir Pusat confirms receipt — no shift/drawer involved.
	received, err := svc.Receive(context.Background(), d.ID, 20)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if received.Status != "received" {
		t.Errorf("expected status received, got %s", received.Status)
	}
	if received.ReceivedBy == nil || *received.ReceivedBy != 20 {
		t.Errorf("expected received_by 20, got %v", received.ReceivedBy)
	}

	// Cannot receive again
	_, err = svc.Receive(context.Background(), d.ID, 20)
	if err != ErrDepositNotPending {
		t.Errorf("expected ErrDepositNotPending, got %v", err)
	}
}
