package distribution

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

	"github.com/shopspring/decimal"
)

func TestCreateDeliveryOrder(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10, 11)
	svc := newService(f)

	o, err := svc.CreateDeliveryOrder(context.Background(), DOInput{
		SalesOrderID: 1,
		Priority:     PriorityHigh,
		Items:        []DOItemInput{{ProductID: 10, RequestedQuantity: decimal.NewFromInt(770)}},
	}, 1)
	if err != nil {
		t.Fatalf("CreateDeliveryOrder: %v", err)
	}
	if o.Status != StatusDraft {
		t.Fatalf("status = %s, want %s", o.Status, StatusDraft)
	}
	if !strings.HasPrefix(o.DoNumber, "DO-") || len(o.DoNumber) < 14 {
		t.Fatalf("do_number %q not a monthly DO code", o.DoNumber)
	}
	if o.WarehouseID != 2 {
		t.Fatalf("warehouse_id = %d, want 2 (from SO)", o.WarehouseID)
	}
	if o.RequestedQuantity.String() != "770" || o.RemainingQuantity.String() != "770" {
		t.Fatalf("requested/remaining = %s/%s", o.RequestedQuantity, o.RemainingQuantity)
	}
	if len(o.Items) != 1 || o.Items[0].ProductID != 10 {
		t.Fatalf("items = %+v", o.Items)
	}

	// draft SO cannot be fulfilled
	f.mustSO(t, 2, 2, "draft", 10)
	if _, err := svc.CreateDeliveryOrder(context.Background(), DOInput{SalesOrderID: 2, Items: []DOItemInput{{ProductID: 10, RequestedQuantity: decimal.NewFromInt(1)}}}, 1); !errors.Is(err, ErrSOInvalidStatus) {
		t.Fatalf("draft SO: err = %v, want ErrSOInvalidStatus", err)
	}

	// item not on the SO
	if _, err := svc.CreateDeliveryOrder(context.Background(), DOInput{SalesOrderID: 1, Items: []DOItemInput{{ProductID: 99, RequestedQuantity: decimal.NewFromInt(1)}}}, 1); !errors.Is(err, ErrItemNotOnSO) {
		t.Fatalf("product not on SO: err = %v, want ErrItemNotOnSO", err)
	}

	// quantity must be positive
	if _, err := svc.CreateDeliveryOrder(context.Background(), DOInput{SalesOrderID: 1, Items: []DOItemInput{{ProductID: 10, RequestedQuantity: decimal.Zero}}}, 1); !errors.Is(err, ErrQuantityRequired) {
		t.Fatalf("zero qty: err = %v, want ErrQuantityRequired", err)
	}
}

func TestPartialShipmentLifecycle(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(770), decimal.NewFromInt(770))
	svc := newService(f)
	f.captureLedger(svc)

	o := f.mustDO(t, svc, 1, 10)

	// first shipment: 500 of 770 -> partially fulfilled
	sh, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}},
	}, 1)
	if err != nil {
		t.Fatalf("CreateShipment(500): %v", err)
	}
	if sh.Status != ShipmentPending || len(sh.Items) != 1 || sh.Items[0].ShippedQuantity.String() != "500" {
		t.Fatalf("shipment = %+v", sh)
	}
	if !strings.HasPrefix(sh.ShipmentNumber, "SHP-") {
		t.Fatalf("shipment_number %q not a monthly SHP code", sh.ShipmentNumber)
	}

	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusPartiallyFulfilled || o.ShippedQuantity.String() != "500" || o.RemainingQuantity.String() != "270" {
		t.Fatalf("after 500: status=%s shipped=%s remaining=%s", o.Status, o.ShippedQuantity, o.RemainingQuantity)
	}
	// stock: on-hand reduced by shipped, reservation released by shipped
	s := f.stocks["10:2"]
	if s.Quantity.String() != "270" || s.ReservedQuantity.String() != "270" {
		t.Fatalf("stock after 500: qty=%s reserved=%s", s.Quantity, s.ReservedQuantity)
	}
	if len(f.shipLedger) != 1 {
		t.Fatalf("ledger calls = %d, want 1", len(f.shipLedger))
	}
	if got := f.shipLedger[0]; got.before.String() != "770" || got.after.String() != "270" || got.qty.String() != "500" {
		t.Fatalf("ledger call = %+v", got)
	}
	// history: draft -> partially_fulfilled, ref shipment
	hist := f.history[o.ID]
	if len(hist) != 1 || hist[0].ToStatus != StatusPartiallyFulfilled {
		t.Fatalf("history = %+v", hist)
	}

	// second shipment makes the DO fully shipped, awaiting delivery
	_, err = svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(270)}},
	}, 1)
	if err != nil {
		t.Fatalf("CreateShipment(270): %v", err)
	}
	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusReady || o.RemainingQuantity.String() != "0" || o.ShippedQuantity.String() != "770" {
		t.Fatalf("after 270: status=%s shipped=%s remaining=%s", o.Status, o.ShippedQuantity, o.RemainingQuantity)
	}
	s = f.stocks["10:2"]
	if s.Quantity.String() != "0" || s.ReservedQuantity.String() != "0" {
		t.Fatalf("stock after full: qty=%s reserved=%s", s.Quantity, s.ReservedQuantity)
	}
	if o.Shipments[0].ShipmentNumber == o.Shipments[1].ShipmentNumber {
		t.Fatal("shipment numbers must be distinct from each other and the DO")
	}
	if o.Shipments[0].ShipmentNumber == o.DoNumber || o.Shipments[1].ShipmentNumber == o.DoNumber {
		t.Fatal("shipment number must differ from the parent DO number")
	}

	// a fully shipped (ready) DO cannot receive more shipments
	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(1)}}}, 1); !errors.Is(err, ErrCannotShipment) {
		t.Fatalf("ship on ready DO: err = %v, want ErrCannotShipment", err)
	}

	// delivery of every shipment closes the DO
	if _, err := svc.DeliverShipment(context.Background(), o.Shipments[1].ID, 1); err != nil {
		t.Fatalf("DeliverShipment(270): %v", err)
	}
	if _, err := svc.DeliverShipment(context.Background(), o.Shipments[0].ID, 1); err != nil {
		t.Fatalf("DeliverShipment(500): %v", err)
	}
	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusCompleted || o.DeliveredQuantity.String() != "770" {
		t.Fatalf("after delivery: status=%s delivered=%s", o.Status, o.DeliveredQuantity)
	}
}

func TestOverShipmentAbortsAtomically(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(770), decimal.NewFromInt(770))
	svc := newService(f)

	o := f.mustDO(t, svc, 1, 10)
	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}},
	}, 1); err != nil {
		t.Fatalf("CreateShipment(500): %v", err)
	}

	before := f.stocks["10:2"].Quantity
	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(400)}},
	}, 1); !errors.Is(err, ErrOverShipment) {
		t.Fatalf("over-ship: err = %v, want ErrOverShipment", err)
	}

	o, err := svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.ShippedQuantity.String() != "500" || o.RemainingQuantity.String() != "270" || o.Status != StatusPartiallyFulfilled {
		t.Fatalf("DO mutated by failed shipment: status=%s shipped=%s remaining=%s", o.Status, o.ShippedQuantity, o.RemainingQuantity)
	}
	if len(o.Shipments) != 1 {
		t.Fatalf("shipments = %d, want 1", len(o.Shipments))
	}
	if got := f.stocks["10:2"].Quantity; !got.Equal(before) {
		t.Fatalf("stock mutated by failed shipment: %s", got)
	}
}

func TestShipmentInsufficientStock(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(100), decimal.NewFromInt(100))
	svc := newService(f)

	o := f.mustDO(t, svc, 1, 10)
	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}},
	}, 1); !errors.Is(err, ErrInsufficientStock) {
		t.Fatalf("short stock: err = %v, want ErrInsufficientStock", err)
	}
	o, err := svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.ShippedQuantity.String() != "0" || len(o.Shipments) != 0 {
		t.Fatalf("failed shipment wrote DO state: %+v", o)
	}
}

func TestDispatchQueueAndReassignmentAudited(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(770), decimal.NewFromInt(770))
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)

	// shipment executed with an inline dispatch -> in_transit, queue 1
	sh, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items:    []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(770)}},
		Dispatch: &DispatchInfo{FleetID: 1, Priority: PriorityHigh},
	}, 1)
	if err != nil {
		t.Fatalf("CreateShipment+dispatch: %v", err)
	}
	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusInTransit {
		t.Fatalf("DO status = %s, want in_transit", o.Status)
	}
	if len(o.Dispatches) != 1 || o.Dispatches[0].QueueOrder != 1 || o.Dispatches[0].FleetID != 1 {
		t.Fatalf("dispatches = %+v", o.Dispatches)
	}

	// reassignment: new dispatch appends queue 2 and an audit row
	d, err := svc.CreateDispatch(context.Background(), o.ID, DispatchInput{
		ShipmentID: sh.ID, FleetID: 2, Priority: PriorityUrgent,
	}, 1)
	if err != nil {
		t.Fatalf("CreateDispatch: %v", err)
	}
	if d.QueueOrder != 2 || d.FleetID != 2 {
		t.Fatalf("reassigned dispatch = %+v", d)
	}
	hist := f.history[o.ID]
	dispatchRefs := 0
	for _, h := range hist {
		if h.ReferenceType.Valid && *textToStrPtr(h.ReferenceType) == "dispatch" && h.ReferenceID.Valid && *int8ToPtr(h.ReferenceID) == d.ID {
			dispatchRefs++
		}
	}
	if dispatchRefs == 0 {
		t.Fatalf("no audit history referencing dispatch %d", d.ID)
	}

	// a delivered shipment cannot be dispatched
	if _, err := svc.DeliverShipment(context.Background(), sh.ID, 1); err != nil {
		t.Fatalf("DeliverShipment: %v", err)
	}
	if _, err := svc.CreateDispatch(context.Background(), o.ID, DispatchInput{ShipmentID: sh.ID, FleetID: 3}, 1); !errors.Is(err, ErrShipmentClosed) {
		t.Fatalf("dispatch on delivered: err = %v, want ErrShipmentClosed", err)
	}
}

func TestDeliverShipment(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(770), decimal.NewFromInt(770))
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)

	sh1, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}}}, 1)
	if err != nil {
		t.Fatal(err)
	}
	sh2, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(270)}}}, 1)
	if err != nil {
		t.Fatal(err)
	}

	// partial delivery keeps the DO open
	if _, err := svc.DeliverShipment(context.Background(), sh1.ID, 1); err != nil {
		t.Fatal(err)
	}
	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusDelivered || o.DeliveredQuantity.String() != "500" {
		t.Fatalf("after first delivery: status=%s delivered=%s", o.Status, o.DeliveredQuantity)
	}

	// last shipment delivered closes the DO
	if _, err := svc.DeliverShipment(context.Background(), sh2.ID, 1); err != nil {
		t.Fatal(err)
	}
	o, err = svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != StatusCompleted || o.DeliveredQuantity.String() != "770" {
		t.Fatalf("after second delivery: status=%s delivered=%s", o.Status, o.DeliveredQuantity)
	}
	if o.Shipments[0].Status != ShipmentDelivered || o.Shipments[1].Status != ShipmentDelivered {
		t.Fatalf("shipment statuses = %s/%s", o.Shipments[0].Status, o.Shipments[1].Status)
	}
	if f.sos[1].Status != "delivered" {
		t.Fatalf("expected sales order to sync to delivered, got %s", f.sos[1].Status)
	}
}

func TestDeliverDeliveryOrder_SyncsSalesOrder(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(500), decimal.NewFromInt(500))
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)

	// Create shipment with dispatch -> SO should sync to shipped
	_, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items:    []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}},
		Dispatch: &DispatchInfo{FleetID: 1, Priority: "normal"},
	}, 1)
	if err != nil {
		t.Fatal(err)
	}
	if f.sos[1].Status != "shipped" {
		t.Fatalf("expected SO to be shipped after dispatch, got %s", f.sos[1].Status)
	}

	// Warehouse admin completes DO directly -> SO should sync to delivered
	completed, err := svc.DeliverDeliveryOrder(context.Background(), o.ID, 1, nil, "")
	if err != nil {
		t.Fatalf("DeliverDeliveryOrder: %v", err)
	}
	if completed.Status != StatusCompleted {
		t.Fatalf("expected DO status completed, got %s", completed.Status)
	}
	if f.sos[1].Status != "delivered" {
		t.Fatalf("expected SO to be delivered, got %s", f.sos[1].Status)
	}
}

func TestDeliverDeliveryOrder_SOSyncFailureStillCompletes(t *testing.T) {
	// A6: the SO sync is best-effort (a vanished SO must not fail the
	// delivery) but never silent — it logs. The delivery must complete.
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(500), decimal.NewFromInt(500))
	f.failSyncSO = true
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)
	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items:    []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(500)}},
		Dispatch: &DispatchInfo{FleetID: 1, Priority: "normal"},
	}, 1); err != nil {
		t.Fatal(err)
	}
	completed, err := svc.DeliverDeliveryOrder(context.Background(), o.ID, 1, nil, "")
	if err != nil {
		t.Fatalf("DeliverDeliveryOrder with failing SO sync: %v", err)
	}
	if completed.Status != StatusCompleted {
		t.Fatalf("status = %s, want completed", completed.Status)
	}
}

func TestFleetCRUD(t *testing.T) {
	f := newFakeStore()
	svc := newService(f)

	v, err := svc.CreateVehicle(context.Background(), VehicleInput{
		PlateNumber: "B 1234 XYZ", VehicleName: "Truck A",
	})
	if err != nil {
		t.Fatal(err)
	}
	if !v.IsActive || v.PlateNumber != "B 1234 XYZ" {
		t.Fatalf("vehicle = %+v", v)
	}
	if _, err := svc.CreateVehicle(context.Background(), VehicleInput{VehicleName: "no plate"}); !errors.Is(err, ErrPlateRequired) {
		t.Fatalf("no plate: err = %v, want ErrPlateRequired", err)
	}
	got, err := svc.GetVehicle(context.Background(), v.ID)
	if err != nil || got.ID != v.ID {
		t.Fatalf("GetVehicle = %+v, err=%v", got, err)
	}
	list, total, err := svc.ListFleet(context.Background(), FleetFilter{})
	if err != nil || total != 1 || len(list) != 1 {
		t.Fatalf("ListFleet = %d/%d, err=%v", len(list), total, err)
	}
}

func TestCreateShipment_EcommerceAlreadyDeducted(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	so := f.sos[1]
	so.OrderType = "ecommerce"
	f.sos[1] = so
	// Stock on hand is 0 (as physical stock was already deducted at e-commerce checkout)
	f.addStock(10, 2, decimal.Zero, decimal.Zero)
	svc := newService(f)
	f.captureLedger(svc)

	o := f.mustDO(t, svc, 1, 10)

	// Creating shipment must succeed without ErrInsufficientStock and not deduct below 0
	sh, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items: []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(5)}},
	}, 1)
	if err != nil {
		t.Fatalf("CreateShipment for ecommerce order should succeed even if stock is 0: %v", err)
	}
	if sh.Status != ShipmentPending || len(sh.Items) != 1 || sh.Items[0].ShippedQuantity.String() != "5" {
		t.Fatalf("unexpected shipment: %+v", sh)
	}
	// Verify stock was not deducted again
	st := f.stocks[stockKey(10, 2)]
	if !st.Quantity.IsZero() {
		t.Fatalf("expected stock quantity to remain 0, got %s", st.Quantity)
	}
}

// Regression (sequential DO flow): an inline dispatch must audit BOTH
// transitions — allocation ("shipment executed") and driver handoff
// ("dispatch assigned") — instead of collapsing into a single
// draft→in_transit history row that hides the packing/allocation step.
func TestInlineDispatchAuditsAllocationAndDispatch(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "confirmed", 10)
	f.addStock(10, 2, decimal.NewFromInt(770), decimal.NewFromInt(770))
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)

	if _, err := svc.CreateShipment(context.Background(), o.ID, ShipmentInput{
		Items:    []ShipmentItemInput{{ProductID: 10, ShippedQuantity: decimal.NewFromInt(770)}},
		Dispatch: &DispatchInfo{FleetID: 1, Priority: PriorityHigh},
	}, 1); err != nil {
		t.Fatalf("CreateShipment+dispatch: %v", err)
	}

	type step struct{ from, to, reason string }
	want := []step{
		{StatusDraft, StatusReady, "shipment executed"},
		{StatusReady, StatusInTransit, "dispatch assigned"},
	}
	hist := f.history[o.ID]
	if len(hist) < len(want) {
		t.Fatalf("history rows = %d, want >= %d: %+v", len(hist), len(want), hist)
	}
	for i, w := range want {
		if hist[i].FromStatus != w.from || hist[i].ToStatus != w.to || hist[i].Reason != w.reason {
			t.Fatalf("history[%d] = %s→%s (%s), want %s→%s (%s)",
				i, hist[i].FromStatus, hist[i].ToStatus, hist[i].Reason, w.from, w.to, w.reason)
		}
	}
}

// The ERP progress stepper reads sales_order_status to light "Sedang
// Dikemas" — the DO row itself stays draft until the first shipment
// allocates quantity.
func TestGetDeliveryOrderHydratesSalesOrderStatus(t *testing.T) {
	f := newFakeStore()
	f.mustSO(t, 1, 2, "picking", 10)
	svc := newService(f)
	o := f.mustDO(t, svc, 1, 10)

	got, err := svc.GetDeliveryOrder(context.Background(), o.ID)
	if err != nil {
		t.Fatal(err)
	}
	if got.SalesOrderStatus != "picking" {
		t.Fatalf("sales_order_status = %q, want picking", got.SalesOrderStatus)
	}

	list, _, err := svc.ListDeliveryOrders(context.Background(), DOFilter{Page: 1, PerPage: 10})
	if err != nil {
		t.Fatal(err)
	}
	if len(list) != 1 || list[0].SalesOrderStatus != "picking" {
		t.Fatalf("list sales_order_status = %+v, want picking", list)
	}
}
