package integration

import (
	"context"
	"errors"
	"sync"
	"testing"

	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/inventory"
	"github.com/TaruLab/keluargamart-backend/internal/sales"
)

func TestPOS_ExactMovementsAndInsufficientRollback(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	salesSvc := salesService(pool, inv)

	wh := seedWarehouse(t, pool)
	cat := seedCategory(t, pool)
	unit := seedUnit(t, pool)
	p1 := seedProduct(t, pool, cat, unit, "100")
	p2 := seedProduct(t, pool, cat, unit, "50")
	seedStock(t, pool, p1.ID, wh, "10")
	seedStock(t, pool, p2.ID, wh, "20")
	cust := seedCustomer(t, pool)

	so, invc, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		CashReceived: decimalPtr("500"),
		Items: []sales.POSItemInput{
			{ProductID: p1.ID, Quantity: decimal.NewFromInt(2), UnitPrice: decimal.NewFromInt(999)},
			{ProductID: p2.ID, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(999)},
		},
	})
	if err != nil {
		t.Fatalf("ProcessPOS: %v", err)
	}
	if !invc.TotalAmount.Equal(decimal.NewFromInt(250)) {
		t.Errorf("invoice total = %s, want 250 (server price, not client 999)", invc.TotalAmount)
	}
	if got := stockQty(t, pool, p1.ID, wh); !got.Equal(decimal.NewFromInt(8)) {
		t.Errorf("p1 stock = %s, want 8", got)
	}
	if got := stockQty(t, pool, p2.ID, wh); !got.Equal(decimal.NewFromInt(19)) {
		t.Errorf("p2 stock = %s, want 19", got)
	}
	if n := movementCount(t, pool); n != 2 {
		t.Fatalf("movements = %d, want 2", n)
	}
	rows, err := pool.Query(ctx,
		`SELECT product_id, quantity_before, quantity_after, quantity FROM stock_movements
		 WHERE reference_type='sales_order' AND reference_id=$1 ORDER BY product_id`, so.ID)
	if err != nil {
		t.Fatal(err)
	}
	defer rows.Close()
	type mval struct {
		before, after, qty decimal.Decimal
	}
	got := map[int64]mval{}
	for rows.Next() {
		var pid int64
		var v mval
		if err := rows.Scan(&pid, &v.before, &v.after, &v.qty); err != nil {
			t.Fatal(err)
		}
		got[pid] = v
	}
	if rows.Err() != nil {
		t.Fatal(rows.Err())
	}
	if v, ok := got[p1.ID]; !ok || !v.before.Equal(decimal.NewFromInt(10)) || !v.after.Equal(decimal.NewFromInt(8)) || !v.qty.Equal(decimal.NewFromInt(2)) {
		t.Errorf("p1 movement = %+v, want before 10 after 8 qty 2", got[p1.ID])
	}
	if v, ok := got[p2.ID]; !ok || !v.before.Equal(decimal.NewFromInt(20)) || !v.after.Equal(decimal.NewFromInt(19)) || !v.qty.Equal(decimal.NewFromInt(1)) {
		t.Errorf("p2 movement = %+v, want before 20 after 19 qty 1", got[p2.ID])
	}

	// In POS, transactions are allowed even when stock is insufficient (stock goes negative).
	// An invalid POS sale (e.g. product not found) must roll back completely: no order, invoice, or movement.
	before := movementCount(t, pool)
	beforeOrders := soCount(t, pool)
	_, _, err = salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{
			{ProductID: 99999, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(100)},
		},
	})
	if !errors.Is(err, sales.ErrNotFound) {
		t.Fatalf("invalid product error = %v, want ErrNotFound", err)
	}
	if n := movementCount(t, pool); n != before {
		t.Errorf("movements after failed POS = %d, want %d", n, before)
	}
	if n := soCount(t, pool); n != beforeOrders {
		t.Errorf("sales_orders after failed POS = %d, want %d", n, beforeOrders)
	}
	if got := stockQty(t, pool, p1.ID, wh); !got.Equal(decimal.NewFromInt(8)) {
		t.Errorf("p1 stock after failed POS = %s, want 8", got)
	}

	// Verify that overselling POS proceeds and drives stock negative (8 - 10 = -2).
	_, _, err = salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{
			{ProductID: p1.ID, Quantity: decimal.NewFromInt(10), UnitPrice: decimal.NewFromInt(100)},
		},
	})
	if err != nil {
		t.Fatalf("oversell POS should succeed, got %v", err)
	}
	if got := stockQty(t, pool, p1.ID, wh); !got.Equal(decimal.NewFromInt(-2)) {
		t.Errorf("p1 stock after oversell POS = %s, want -2", got)
	}
}

func TestConcurrentPOS_PermissiveStock(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	salesSvc := salesService(pool, inv)

	wh := seedWarehouse(t, pool)
	cat := seedCategory(t, pool)
	unit := seedUnit(t, pool)
	p := seedProduct(t, pool, cat, unit, "100")
	seedStock(t, pool, p.ID, wh, "10")
	cust := seedCustomer(t, pool)

	const workers = 5
	const qty = 3 // 5x3 = 15 > 10 stock
	var wg sync.WaitGroup
	var mu sync.Mutex
	var successes, failures int
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, _, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
				CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
				Items: []sales.POSItemInput{{ProductID: p.ID, Quantity: decimal.NewFromInt(qty), UnitPrice: decimal.NewFromInt(100)}},
			})
			mu.Lock()
			defer mu.Unlock()
			if err != nil {
				failures++
				t.Errorf("unexpected POS error: %v", err)
				return
			}
			successes++
		}()
	}
	wg.Wait()

	if successes != 5 || failures != 0 {
		t.Errorf("successes=%d failures=%d, want 5/0 (POS allows zero/negative stock)", successes, failures)
	}
	if got := stockQty(t, pool, p.ID, wh); !got.Equal(decimal.NewFromInt(-5)) {
		t.Errorf("final stock = %s, want -5", got)
	}
	if n := movementCount(t, pool); n != 5 {
		t.Errorf("movements = %d, want 5", n)
	}
}
