package inventory

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

	"github.com/shopspring/decimal"

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

// "Perhitungan selisihnya rancu, tidak mengikuti stok di bagian opname."
//
// Two defects behind that report:
//  1. A line nobody had counted was reported as a full negative variance
//     (COALESCE(physical,0) - system), so an untouched sheet read as if every
//     product were missing.
//  2. "Terbitkan & Kunci Stok" only set a status: ordinary movements kept
//     changing stock underneath a running count, so the baseline the team was
//     counting against had already moved.
func TestOpname_DifferenceIsEmptyUntilCounted(t *testing.T) {
	svc, fs := opnameTestService(t)

	o, err := svc.OpnameCreate(context.Background(), OpnameInput{WarehouseID: 1, OpnameDate: "2026-11-01"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	items, err := svc.OpnameItems(context.Background(), o.ID)
	if err != nil {
		t.Fatalf("items: %v", err)
	}
	if len(items) == 0 {
		t.Fatal("opname has no snapshot items")
	}
	for _, it := range items {
		if it.Difference != nil {
			t.Errorf("uncounted line %s reports a difference, want nil", it.ProductCode)
		}
	}

	// Count one line short by 3: only that line gains a difference.
	var target int64
	for _, it := range items {
		if it.SystemQuantity.GreaterThan(decimal.NewFromInt(3)) {
			target = it.ProductID
			break
		}
	}
	if target == 0 {
		t.Skip("fixture has no product with stock > 3")
	}
	svc2 := svc
	var phys decimal.Decimal
	for _, it := range items {
		if it.ProductID == target {
			phys = it.SystemQuantity.Sub(decimal.NewFromInt(3))
		}
	}
	if err := svc2.OpnameSetPhysical(context.Background(), o.ID, []OpnameItemInput{
		{ProductID: target, PhysicalQty: &phys},
	}); err != nil {
		t.Fatalf("set physical: %v", err)
	}

	items, _ = svc.OpnameItems(context.Background(), o.ID)
	for _, it := range items {
		if it.ProductID == target {
			if it.Difference == nil {
				t.Fatal("counted line lost its difference")
			}
			if !it.Difference.Equal(decimal.NewFromInt(-3)) {
				t.Errorf("difference = %s, want -3", it.Difference)
			}
		} else if it.Difference != nil {
			t.Errorf("line %s became a variance without being counted", it.ProductCode)
		}
	}
	_ = fs
}

func TestOpname_FreezeStockBlocksOrdinaryMovements(t *testing.T) {
	svc, fs := opnameTestService(t)

	o, err := svc.OpnameCreate(context.Background(), OpnameInput{WarehouseID: 1, OpnameDate: "2026-11-01"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	// The warehouse now has an open opname: its stock is frozen.
	fs.frozenWh = map[int64]bool{1: true}

	refType := "manual"
	_ = refType
	if _, err := svc.StockIn(context.Background(), StockInInput{
		ProductID: 1, WarehouseID: 1, Quantity: decimal.NewFromInt(1), Notes: strPtrInv("uji"),
	}); !errors.Is(err, ErrStockFrozen) {
		t.Fatalf("stock-in during freeze = %v, want ErrStockFrozen", err)
	}

	// The opname's own correction must still be allowed, or finalizing could
	// never write the variance it just measured.
	fs.frozenWh = map[int64]bool{1: false}
	phys := decimal.Zero
	if err := svc.OpnameSetPhysical(context.Background(), o.ID, []OpnameItemInput{
		{ProductID: 1, PhysicalQty: &phys},
	}); err != nil {
		t.Fatalf("set physical: %v", err)
	}
	fs.frozenWh = map[int64]bool{1: true}
	if _, err := svc.OpnameFinalize(context.Background(), o.ID); err != nil {
		t.Fatalf("finalize while frozen must still apply its own adjustment: %v", err)
	}
}

// --- fixture -----------------------------------------------------------------

func opnameTestService(t *testing.T) (*Service, *fakeStore) {
	t.Helper()
	fs := newFakeStore()
	svc := NewService(fs, &fakeSeq{}, time.UTC)
	svc.WithAllowNegativeStock(true)

	// Two products with stock in warehouse 1.
	fs.stocks["1:1"] = &Stock{ID: 1, ProductID: 1, WarehouseID: 1, Quantity: decimal.NewFromInt(10)}
	fs.stocks["2:1"] = &Stock{ID: 2, ProductID: 2, WarehouseID: 1, Quantity: decimal.NewFromInt(0)}
	fs.lines = []StockLine{
		{ProductID: 1, ProductCode: "SKU-1", ProductName: "Produk 1", UnitSymbol: "pcs", Quantity: decimal.NewFromInt(10), IsActive: true},
		{ProductID: 2, ProductCode: "SKU-2", ProductName: "Produk 2", UnitSymbol: "pcs", Quantity: decimal.Zero, IsActive: true},
	}
	fs.whsForStores = map[int64][]int64{}
	return svc, fs
}

func strPtrInv(s string) *string { return &s }

var _ = auth.UserFrom

// The freeze must not stop the till. Sales deduct stock through internal/sales'
// own locked path, so this guard (which lives on adjustStockOn) never sees a
// sale — assert the scoping explicitly so a later refactor cannot quietly move
// sales onto this path and freeze every store with an open opname.
func TestFreezeStock_DoesNotCoverSalesPath(t *testing.T) {
	svc, fs := opnameTestService(t)
	fs.frozenWh = map[int64]bool{1: true}
	if _, err := svc.OpnameCreate(context.Background(), OpnameInput{WarehouseID: 9, OpnameDate: "2026-11-01"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	// A warehouse-side movement IS blocked while frozen...
	if _, err := svc.StockIn(context.Background(), StockInInput{
		ProductID: 1, WarehouseID: 1, Quantity: decimal.NewFromInt(1),
	}); !errors.Is(err, ErrStockFrozen) {
		t.Fatalf("stock-in on a frozen warehouse = %v, want ErrStockFrozen", err)
	}
	// ...and the same warehouse still accepts the opname's own adjustment.
	o := fs.onlyOpnameFor(t, 9)
	phys := decimal.NewFromInt(1)
	if err := svc.OpnameSetPhysical(context.Background(), o.ID, []OpnameItemInput{{ProductID: 1, PhysicalQty: &phys}}); err != nil {
		t.Fatalf("set physical: %v", err)
	}
	if _, err := svc.OpnameFinalize(context.Background(), o.ID); err != nil {
		t.Fatalf("finalize: %v", err)
	}
}

func (f *fakeStore) onlyOpnameFor(t *testing.T, warehouseID int64) *Opname {
	t.Helper()
	for _, o := range f.opnames {
		if o.WarehouseID == warehouseID {
			return o
		}
	}
	t.Fatalf("no opname for warehouse %d", warehouseID)
	return nil
}
