package integration

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"

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

func seedProductUnit(t *testing.T, pool *pgxpool.Pool, productID, unitID int64, factor, price, minPurchase string, isBase bool) int64 {
	t.Helper()
	var id int64
	var sellingPrice any
	if price != "" {
		sellingPrice = price
	}
	if err := pool.QueryRow(context.Background(),
		`INSERT INTO product_units (product_id, unit_id, conversion_factor, selling_price, min_purchase, is_base, is_active)
		 VALUES ($1, $2, $3, $4, $5, $6, true) RETURNING id`,
		productID, unitID, factor, sellingPrice, minPurchase, isBase).Scan(&id); err != nil {
		t.Fatal(err)
	}
	return id
}

// TestPricingSnapshotGate is the Phase 6 item 2 gate (§11.2, §12): the backend
// resolves the configured product-unit price and minimum purchase, stock moves
// in base units via the conversion factor, the client price is ignored, a
// below-minimum line aborts the whole sale, and the persisted line snapshot is
// immutable.
func TestPricingSnapshotGate(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)
	pcs := seedUnit(t, pool)
	box := seedUnit(t, pool)
	prod := seedProduct(t, pool, cat, pcs, "12000")
	seedStock(t, pool, prod.ID, wh, "100")
	cust := seedCustomer(t, pool)

	baseUnitID := seedProductUnit(t, pool, prod.ID, pcs, "1", "", "1", true)
	boxUnitID := seedProductUnit(t, pool, prod.ID, box, "12", "138000", "2", false)

	// a below-minimum box line must abort before any stock or order is written
	_, _, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{{ProductID: prod.ID, ProductUnitID: &boxUnitID, Quantity: decimal.NewFromInt(1)}},
	})
	if !errors.Is(err, pricing.ErrBelowMinPurchase) {
		t.Fatalf("below-minimum POS got %v, want ErrBelowMinPurchase", err)
	}
	if got := stockQty(t, pool, prod.ID, wh); !got.Equal(decimal.NewFromInt(100)) {
		t.Fatalf("stock = %s after rejected sale, want 100 (no partial write)", got)
	}
	if n := soCount(t, pool); n != 0 {
		t.Fatalf("sales orders = %d after rejected sale, want 0", n)
	}

	// 2 boxes at the configured 138000/box; client price 1 must be ignored
	so, invc, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{
			{ProductID: prod.ID, ProductUnitID: &boxUnitID, Quantity: decimal.NewFromInt(2), UnitPrice: decimal.NewFromInt(1)},
		},
	})
	if err != nil {
		t.Fatalf("ProcessPOS: %v", err)
	}
	if !invc.TotalAmount.Equal(decimal.NewFromInt(276000)) {
		t.Errorf("invoice total = %s, want 276000 (138000 x 2 boxes)", invc.TotalAmount)
	}
	// 2 boxes x factor 12 = 24 base units: 100 -> 76
	if got := stockQty(t, pool, prod.ID, wh); !got.Equal(decimal.NewFromInt(76)) {
		t.Errorf("stock = %s, want 76 (24 base units deducted)", got)
	}

	var (
		lineID     int64
		puID       int64
		unitID     int64
		unitName   string
		unitSymbol string
		factor     decimal.Decimal
		baseQty    decimal.Decimal
		minPurch   decimal.Decimal
		unitPrice  decimal.Decimal
		quantity   decimal.Decimal
	)
	if err := pool.QueryRow(ctx,
		`SELECT id, product_unit_id, unit_id, unit_name, unit_symbol, conversion_factor,
		        base_quantity, min_purchase, unit_price, quantity
		 FROM sales_order_items WHERE sales_order_id = $1`, so.ID).
		Scan(&lineID, &puID, &unitID, &unitName, &unitSymbol, &factor, &baseQty, &minPurch, &unitPrice, &quantity); err != nil {
		t.Fatal(err)
	}
	if puID != boxUnitID || unitID != box {
		t.Errorf("snapshot units = %d/%d, want %d/%d", puID, unitID, boxUnitID, box)
	}
	if !factor.Equal(decimal.NewFromInt(12)) || !baseQty.Equal(decimal.NewFromInt(24)) {
		t.Errorf("factor/base_quantity = %s/%s, want 12/24", factor, baseQty)
	}
	if !minPurch.Equal(decimal.NewFromInt(2)) {
		t.Errorf("min_purchase = %s, want 2", minPurch)
	}
	if !unitPrice.Equal(decimal.NewFromInt(138000)) {
		t.Errorf("unit_price = %s, want 138000 (client price ignored)", unitPrice)
	}
	if !quantity.Equal(decimal.NewFromInt(2)) {
		t.Errorf("quantity = %s, want 2 (expressed in boxes)", quantity)
	}
	if unitName == "" || unitSymbol == "" {
		t.Errorf("unit name/symbol = %q/%q, want the configured unit", unitName, unitSymbol)
	}

	// the movement must record base units, matching the stock mutation
	var movementQty decimal.Decimal
	if err := pool.QueryRow(ctx,
		`SELECT quantity FROM stock_movements WHERE reference_type='sales_order' AND reference_id=$1`, so.ID).
		Scan(&movementQty); err != nil {
		t.Fatal(err)
	}
	if !movementQty.Equal(decimal.NewFromInt(24)) {
		t.Errorf("movement quantity = %s, want 24 base units", movementQty)
	}

	// the snapshot is append-only: the DB rejects a repriced line
	if _, err := pool.Exec(ctx,
		`UPDATE sales_order_items SET unit_price = 1 WHERE id = $1`, lineID); err == nil {
		t.Fatal("repricing a sales_order_items line must be rejected")
	} else if !strings.Contains(err.Error(), "immutable") {
		t.Fatalf("unexpected error repricing line: %v", err)
	}
	if _, err := pool.Exec(ctx,
		`UPDATE sales_order_items SET base_quantity = 1 WHERE id = $1`, lineID); err == nil {
		t.Fatal("changing base_quantity must be rejected")
	}

	// omitting product_unit_id falls back to the configured base unit
	so2, invc2, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{{ProductID: prod.ID, Quantity: decimal.NewFromInt(3)}},
	})
	if err != nil {
		t.Fatalf("base-unit POS: %v", err)
	}
	if !invc2.TotalAmount.Equal(decimal.NewFromInt(36000)) {
		t.Errorf("base-unit invoice total = %s, want 36000 (12000 x 3)", invc2.TotalAmount)
	}
	var basePU int64
	var baseFactor, baseBaseQty decimal.Decimal
	if err := pool.QueryRow(ctx,
		`SELECT product_unit_id, conversion_factor, base_quantity FROM sales_order_items WHERE sales_order_id = $1`, so2.ID).
		Scan(&basePU, &baseFactor, &baseBaseQty); err != nil {
		t.Fatal(err)
	}
	if basePU != baseUnitID {
		t.Errorf("fallback product_unit_id = %d, want base unit %d", basePU, baseUnitID)
	}
	if !baseFactor.Equal(decimal.NewFromInt(1)) || !baseBaseQty.Equal(decimal.NewFromInt(3)) {
		t.Errorf("base factor/quantity = %s/%s, want 1/3", baseFactor, baseBaseQty)
	}
	if got := stockQty(t, pool, prod.ID, wh); !got.Equal(decimal.NewFromInt(73)) {
		t.Errorf("stock = %s, want 73", got)
	}

	// a unit belonging to another product is rejected
	other := seedProduct(t, pool, cat, pcs, "5000")
	foreignUnit := seedProductUnit(t, pool, other.ID, box, "6", "30000", "1", false)
	if _, _, err := salesSvc.ProcessPOS(ctx, sales.POSInput{
		CustomerID: cust, WarehouseID: wh, PaymentMethod: "cash",
		Items: []sales.POSItemInput{{ProductID: prod.ID, ProductUnitID: &foreignUnit, Quantity: decimal.NewFromInt(1)}},
	}); !errors.Is(err, pricing.ErrUnitNotForProduct) {
		t.Fatalf("foreign unit got %v, want ErrUnitNotForProduct", err)
	}
}
