package integration

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

	"github.com/TaruLab/keluargamart-backend/internal/cart"
	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/TaruLab/keluargamart-backend/internal/inventory"
	"github.com/TaruLab/keluargamart-backend/internal/pricing"
	"github.com/TaruLab/keluargamart-backend/internal/sales"
	"github.com/shopspring/decimal"
)

func TestCheckoutIntegrity_DurableReplayAndRollback(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	svc := salesService(pool, inv)
	checkout := cartService(pool, svc)
	wh := seedWarehouse(t, pool)
	p := seedProduct(t, pool, seedCategory(t, pool), seedUnit(t, pool), "125")
	seedStock(t, pool, p.ID, wh, "10")
	user := seedUser(t, pool)
	if _, err := checkout.AddItem(ctx, user, p.Code, decimal.NewFromInt(2)); err != nil {
		t.Fatal(err)
	}
	in := cart.CheckoutInput{WarehouseID: wh, PaymentMethod: "cash", IdempotencyKey: unique("checkout")}
	order, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", in)
	if err != nil {
		t.Fatal(err)
	}
	var count int
	var total, qty decimal.Decimal
	if err := pool.QueryRow(ctx, `SELECT count(*), sum(subtotal), sum(quantity) FROM sales_order_items WHERE sales_order_id=$1`, order.ID).Scan(&count, &total, &qty); err != nil {
		t.Fatal(err)
	}
	if count != 1 || !total.Equal(decimal.NewFromInt(250)) || !qty.Equal(decimal.NewFromInt(2)) {
		t.Fatalf("invalid item snapshot count=%d total=%s qty=%s", count, total, qty)
	}
	if !order.TotalAmount.Equal(total) || !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(8)) {
		t.Fatal("order/stock mismatch")
	}
	replay, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", in)
	if err != nil || replay.ID != order.ID {
		t.Fatalf("durable replay: order=%v err=%v", replay, err)
	}
	if !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(8)) {
		t.Fatal("replay deducted stock")
	}
	changed := in
	changed.PaymentMethod = "transfer"
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", changed); !errors.Is(err, cart.ErrCheckoutConflict) {
		t.Fatalf("changed payload=%v", err)
	}

	// Fail after the item and stock write, proving the transaction—not a fake
	// store's no-op Rollback—restores the cart, stock, order and receipt.
	if _, err := checkout.AddItem(ctx, user, p.Code, decimal.NewFromInt(1)); err != nil {
		t.Fatal(err)
	}
	fail := errors.New("injected movement failure")
	svc.WithPOSLedger(func(context.Context, db.DBTX, int64, int64, int64, decimal.Decimal, decimal.Decimal, decimal.Decimal) error {
		return fail
	})
	before := soCount(t, pool)
	failedInput := in
	failedInput.IdempotencyKey = unique("rollback")
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", failedInput); !errors.Is(err, fail) {
		t.Fatalf("failure=%v", err)
	}
	if soCount(t, pool) != before || !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(8)) {
		t.Fatal("partial checkout persisted")
	}
	view, err := checkout.GetCart(ctx, user)
	if err != nil || len(view.Items) != 1 {
		t.Fatalf("cart lost: %v %v", view, err)
	}
	if err := pool.QueryRow(ctx, `SELECT count(*) FROM checkout_receipts WHERE user_id=$1 AND idempotency_key=$2`, user, failedInput.IdempotencyKey).Scan(&count); err != nil || count != 0 {
		t.Fatalf("failed receipt persisted count=%d err=%v", count, err)
	}
	svc.WithPOSLedger(nil)
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", failedInput); err != nil {
		t.Fatalf("retry rollback=%v", err)
	}
}

func TestCheckoutIntegrity_ConcurrentKeysAndPermissiveStock(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	svc := salesService(pool, inv).WithAllowNegativeStock(true)
	checkout := cartService(pool, svc)
	wh := seedWarehouse(t, pool)
	p := seedProduct(t, pool, seedCategory(t, pool), seedUnit(t, pool), "100")
	seedStock(t, pool, p.ID, wh, "3")
	user := seedUser(t, pool)
	if _, err := checkout.AddItem(ctx, user, p.Code, decimal.NewFromInt(4)); err != nil {
		t.Fatal(err)
	}
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", cart.CheckoutInput{WarehouseID: wh}); err != nil {
		t.Fatalf("permissive B2B checkout should succeed: %v", err)
	}
	if !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(-1)) {
		t.Fatalf("expected stock to go negative (-1), got %s", stockQty(t, pool, p.ID, wh))
	}
	if _, err := checkout.AddItem(ctx, user, p.Code, decimal.NewFromInt(2)); err != nil {
		t.Fatal(err)
	}
	before := soCount(t, pool)
	var wg sync.WaitGroup
	results := make(chan error, 2)
	for range 2 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", cart.CheckoutInput{WarehouseID: wh, IdempotencyKey: unique("concurrent")})
			results <- err
		}()
	}
	wg.Wait()
	close(results)
	successes := 0
	for err := range results {
		if err == nil {
			successes++
		} else if !errors.Is(err, cart.ErrEmptyCart) {
			t.Fatal(err)
		}
	}
	if successes != 1 || soCount(t, pool) != before+1 || !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(-3)) {
		t.Fatalf("concurrent checkout successes=%d", successes)
	}
}

func TestCheckoutIntegrity_MinimumAndWarehouseValidation(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	checkout := cartService(pool, salesService(pool, inv))
	wh := seedWarehouse(t, pool)
	unit := seedUnit(t, pool)
	p := seedProduct(t, pool, seedCategory(t, pool), unit, "100")
	seedProductUnit(t, pool, p.ID, unit, "1", "100", "5", true)
	seedStock(t, pool, p.ID, wh, "20")
	user := seedUser(t, pool)
	if _, err := checkout.AddItem(ctx, user, p.Code, decimal.NewFromInt(1)); err != nil {
		t.Fatal(err)
	}
	before := soCount(t, pool)
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", cart.CheckoutInput{WarehouseID: wh}); !errors.Is(err, pricing.ErrBelowMinPurchase) {
		t.Fatalf("minimum error=%v", err)
	}
	if soCount(t, pool) != before || !stockQty(t, pool, p.ID, wh).Equal(decimal.NewFromInt(20)) {
		t.Fatal("invalid checkout mutated order/stock")
	}
	if _, err := checkout.UpdateQuantity(ctx, user, p.ID, decimal.NewFromInt(5)); err != nil {
		t.Fatal(err)
	}
	if _, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", cart.CheckoutInput{WarehouseID: -1}); !errors.Is(err, sales.ErrCheckoutValidation) {
		t.Fatalf("warehouse error=%v", err)
	}
	order, err := checkout.Checkout(ctx, user, "Buyer", "buyer@test.local", cart.CheckoutInput{WarehouseID: wh, PaymentMethod: "cash"})
	if err != nil || !order.TotalAmount.Equal(decimal.NewFromInt(500)) {
		t.Fatalf("valid configured pricing: order=%v err=%v", order, err)
	}
}

// Cart mutations use real transactions and lock the cart row; an Exec-only
// query fake cannot exercise their missing-pair or commit behavior.
func TestCartIntegrity_ItemMutations(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	ctx := context.Background()
	repo := cart.NewRepository(pool)
	c, err := repo.GetOrCreateCart(ctx, seedUser(t, pool))
	if err != nil {
		t.Fatal(err)
	}
	other, err := repo.GetOrCreateCart(ctx, seedUser(t, pool))
	if err != nil {
		t.Fatal(err)
	}
	p := seedProduct(t, pool, seedCategory(t, pool), seedUnit(t, pool), "100")
	if _, err := repo.AddOrUpdateItem(ctx, other.ID, p.ID, decimal.NewFromInt(1)); err != nil {
		t.Fatal(err)
	}

	// The product exists in another cart, but this cart/product pair does not.
	if err := repo.UpdateItemQuantity(ctx, c.ID, p.ID, decimal.NewFromInt(3)); !errors.Is(err, cart.ErrItemNotFound) {
		t.Fatalf("update missing pair: err=%v, want ErrItemNotFound", err)
	}
	if err := repo.RemoveItem(ctx, c.ID, p.ID); !errors.Is(err, cart.ErrItemNotFound) {
		t.Fatalf("remove missing pair: err=%v, want ErrItemNotFound", err)
	}
	if _, err := repo.AddOrUpdateItem(ctx, c.ID, p.ID, decimal.NewFromInt(1)); err != nil {
		t.Fatal(err)
	}
	if err := repo.UpdateItemQuantity(ctx, c.ID, p.ID, decimal.NewFromInt(3)); err != nil {
		t.Fatal(err)
	}
	items, err := repo.ListCartItems(ctx, c.ID)
	if err != nil || len(items) != 1 || !items[0].Quantity.Equal(decimal.NewFromInt(3)) {
		t.Fatalf("updated cart: items=%v err=%v", items, err)
	}
	if err := repo.RemoveItem(ctx, c.ID, p.ID); err != nil {
		t.Fatal(err)
	}
	items, err = repo.ListCartItems(ctx, c.ID)
	if err != nil || len(items) != 0 {
		t.Fatalf("removed cart: items=%v err=%v", items, err)
	}
	if _, err := repo.AddOrUpdateItem(ctx, c.ID, p.ID, decimal.NewFromInt(2)); err != nil {
		t.Fatal(err)
	}
	if err := repo.ClearCart(ctx, c.ID); err != nil {
		t.Fatal(err)
	}
	if err := repo.ClearCart(ctx, c.ID); err != nil {
		t.Fatalf("clear empty cart: %v", err)
	}
	items, err = repo.ListCartItems(ctx, c.ID)
	if err != nil || len(items) != 0 {
		t.Fatalf("cleared cart: items=%v err=%v", items, err)
	}
	items, err = repo.ListCartItems(ctx, other.ID)
	if err != nil || len(items) != 1 || !items[0].Quantity.Equal(decimal.NewFromInt(1)) {
		t.Fatalf("other cart changed: items=%v err=%v", items, err)
	}
}
