package integration

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

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/inventory"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"
)

type transferFixture struct {
	pool                     *pgxpool.Pool
	svc                      *inventory.Service
	ctx                      context.Context
	actor, from, to, product int64
}

func transferSetup(t *testing.T, quantity string) transferFixture {
	t.Helper()
	p := testPool(t)
	setUp(t, p)
	f := transferFixture{pool: p, actor: seedUser(t, p), from: seedWarehouse(t, p), to: seedWarehouse(t, p)}
	f.product = seedProduct(t, p, seedCategory(t, p), seedUnit(t, p), "1000").ID
	seedStock(t, p, f.product, f.from, quantity)
	seedStock(t, p, f.product, f.to, "0")
	f.ctx = auth.WithStoreScope(auth.ToContext(context.Background(), &auth.User{ID: f.actor, Role: auth.RoleAdmin}, 0), auth.NewStoreScope(nil))
	f.svc = inventory.NewService(inventory.NewRepository(p), seqAdapter{p}, time.UTC)
	if _, err := p.Exec(f.ctx, `INSERT INTO app_settings(key,value) VALUES('system','{"store":{"name":"Transfer Integration","address":"Local fixture"}}') ON CONFLICT(key) DO UPDATE SET value=EXCLUDED.value`); err != nil {
		t.Fatal(err)
	}
	return f
}
func (f transferFixture) draft(t *testing.T, qty int64) *inventory.Transfer {
	t.Helper()
	tr, err := f.svc.TransferCreate(f.ctx, inventory.TransferInput{FromWarehouseID: f.from, ToWarehouseID: f.to, TransferDate: "2026-10-26", Items: []inventory.TransferItemInput{{ProductID: f.product, QuantityRequested: decimal.NewFromInt(qty)}}})
	if err != nil {
		t.Fatal(err)
	}
	return tr
}
func (f transferFixture) balance(t *testing.T, from, to int64) {
	t.Helper()
	if !stockQty(t, f.pool, f.product, f.from).Equal(decimal.NewFromInt(from)) || !stockQty(t, f.pool, f.product, f.to).Equal(decimal.NewFromInt(to)) {
		t.Fatalf("stock not %d/%d", from, to)
	}
}
func receipt(tr *inventory.Transfer, key string, good, damaged, missing int64) inventory.TransferReceiptInput {
	return inventory.TransferReceiptInput{RequestKey: key, Reason: "Physical count discrepancy", Items: []inventory.TransferReceiptItem{{ItemID: tr.Items[0].ID, Good: decimal.NewFromInt(good), Damaged: decimal.NewFromInt(damaged), Missing: decimal.NewFromInt(missing)}}}
}

func TestTransferIntegrityNormalAndRetry(t *testing.T) {
	f := transferSetup(t, "20")
	tr := f.draft(t, 5)
	f.balance(t, 20, 0)
	if !tr.Items[0].QuantityTransit.IsZero() {
		t.Fatal("draft transit")
	}
	var err error
	tr, err = f.svc.TransferApprove(f.ctx, tr.ID, f.actor)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 15, 0)
	if tr.Status != "in_transit" || !tr.Items[0].QuantityTransit.Equal(decimal.NewFromInt(5)) || tr.VoucherNumber == nil {
		t.Fatalf("invalid dispatch %+v", tr)
	}
	snapshot := string(tr.VoucherSnapshot)
	voucher := *tr.VoucherNumber
	if _, err = f.svc.TransferApprove(f.ctx, tr.ID, f.actor); err != nil {
		t.Fatal(err)
	}
	f.balance(t, 15, 0)
	in := receipt(tr, "receipt-normal", 5, 0, 0)
	tr, err = f.svc.TransferReceive(f.ctx, tr.ID, in, false)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 15, 5)
	if tr.Status != "completed" || !tr.Items[0].QuantityTransit.IsZero() {
		t.Fatal("normal receipt incomplete")
	}
	if _, err = f.svc.TransferReceive(f.ctx, tr.ID, in, false); err != nil {
		t.Fatal(err)
	}
	f.balance(t, 15, 5)
	reread, err := f.svc.TransferByID(f.ctx, tr.ID)
	if err != nil {
		t.Fatal(err)
	}
	if snapshot != string(reread.VoucherSnapshot) || voucher != *reread.VoucherNumber {
		t.Fatal("voucher changed")
	}
	if movementCount(t, f.pool) != 2 {
		t.Fatal("duplicate or missing transfer movements")
	}
	var referenced int
	if err = f.pool.QueryRow(f.ctx, `SELECT count(*) FROM stock_movements WHERE reference_type='stock_transfer' AND reference_id=$1`, tr.ID).Scan(&referenced); err != nil || referenced != 2 {
		t.Fatalf("movement linkage %d %v", referenced, err)
	}
	if _, err = f.svc.TransferEdit(f.ctx, tr.ID, inventory.TransferInput{}, true); !errors.Is(err, inventory.ErrUnsupportedState) {
		t.Fatalf("sent cancellation %v", err)
	}
}

func TestTransferIntegrityPartialLossAndExcess(t *testing.T) {
	f := transferSetup(t, "20")
	tr := f.draft(t, 10)
	tr, err := f.svc.TransferApprove(f.ctx, tr.ID, f.actor)
	if err != nil {
		t.Fatal(err)
	}
	if _, err = f.svc.TransferReceive(f.ctx, tr.ID, receipt(tr, "excess", 11, 0, 0), false); err == nil {
		t.Fatal("excess accepted")
	}
	f.balance(t, 10, 0)
	emptyReason := receipt(tr, "reason-required", 8, 0, 2)
	emptyReason.Reason = ""
	if _, err = f.svc.TransferReceive(f.ctx, tr.ID, emptyReason, false); err == nil {
		t.Fatal("missing discrepancy reason accepted")
	}
	tr, err = f.svc.TransferReceive(f.ctx, tr.ID, receipt(tr, "partial", 8, 0, 2), false)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 10, 8)
	changed := receipt(tr, "partial", 7, 0, 2)
	if _, err = f.svc.TransferReceive(f.ctx, tr.ID, changed, false); err == nil {
		t.Fatal("retry key reused with changed quantities")
	}
	if tr.Status == "completed" || !tr.Items[0].QuantityTransit.Equal(decimal.NewFromInt(2)) {
		t.Fatal("two missing units disappeared")
	}
	noScope := auth.ToContext(context.Background(), &auth.User{ID: f.actor, Role: auth.RoleAdmin}, 0)
	if _, err = f.svc.TransferReceive(noScope, tr.ID, receipt(tr, "scope-absent", 2, 0, 0), false); !errors.Is(err, inventory.ErrForbiddenWarehouse) {
		t.Fatalf("absent scope %v", err)
	}
	wrong := auth.WithStoreScope(f.ctx, auth.NewStoreScope([]int64{-99}))
	if _, err = f.svc.TransferReceive(wrong, tr.ID, receipt(tr, "wrong-destination", 2, 0, 0), false); !errors.Is(err, inventory.ErrForbiddenWarehouse) {
		t.Fatalf("foreign destination %v", err)
	}
	worker := auth.WithStoreScope(auth.ToContext(context.Background(), &auth.User{ID: f.actor, Role: auth.RoleWarehouse}, 0), auth.NewStoreScope(nil))
	if _, err = f.svc.TransferReceive(worker, tr.ID, receipt(tr, "unauthorized-loss", 0, 0, 2), true); !errors.Is(err, inventory.ErrForbiddenWarehouse) {
		t.Fatalf("unauthorized loss %v", err)
	}
	tr, err = f.svc.TransferReceive(f.ctx, tr.ID, receipt(tr, "loss", 0, 0, 2), true)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 10, 8)
	if tr.Status != "completed" || !tr.Items[0].QuantityTransit.IsZero() || !tr.Items[0].QuantityLost.Equal(decimal.NewFromInt(2)) {
		t.Fatal("loss not audited/accounted")
	}
	if len(tr.Events) != 4 || tr.Events[3].Action != "reconcile" || tr.Events[3].ActorID != f.actor {
		t.Fatal("missing reconciliation audit")
	}
}

func TestTransferIntegrityInsufficientAndConcurrentDispatch(t *testing.T) {
	f := transferSetup(t, "3")
	tr := f.draft(t, 5)
	if _, err := f.svc.TransferApprove(f.ctx, tr.ID, f.actor); !errors.Is(err, inventory.ErrInsufficient) {
		t.Fatalf("insufficient %v", err)
	}
	f.balance(t, 3, 0)
	unchanged, err := f.svc.TransferByID(f.ctx, tr.ID)
	if err != nil {
		t.Fatal(err)
	}
	if unchanged.Status != "draft" || movementCount(t, f.pool) != 0 {
		t.Fatal("partial insufficient dispatch")
	}
	if _, err = f.pool.Exec(f.ctx, `UPDATE stocks SET quantity=5 WHERE product_id=$1 AND warehouse_id=$2`, f.product, f.from); err != nil {
		t.Fatal(err)
	}
	second := f.draft(t, 5)
	var wg sync.WaitGroup
	results := make(chan error, 2)
	for _, id := range []int64{tr.ID, second.ID} {
		wg.Add(1)
		go func(id int64) { defer wg.Done(); _, e := f.svc.TransferApprove(f.ctx, id, f.actor); results <- e }(id)
	}
	wg.Wait()
	close(results)
	success, insufficient := 0, 0
	for e := range results {
		if e == nil {
			success++
		} else if errors.Is(e, inventory.ErrInsufficient) {
			insufficient++
		} else {
			t.Fatal(e)
		}
	}
	if success != 1 || insufficient != 1 {
		t.Fatalf("concurrent dispatch success=%d insufficient=%d", success, insufficient)
	}
	f.balance(t, 0, 0)
	if movementCount(t, f.pool) != 1 {
		t.Fatal("concurrent oversell")
	}
}

func TestTransferIntegrityTransactionRollback(t *testing.T) {
	for _, receiptFailure := range []bool{false, true} {
		t.Run(map[bool]string{false: "dispatch", true: "receipt"}[receiptFailure], func(t *testing.T) {
			f := transferSetup(t, "20")
			tr := f.draft(t, 5)
			var err error
			if receiptFailure {
				tr, err = f.svc.TransferApprove(f.ctx, tr.ID, f.actor)
				if err != nil {
					t.Fatal(err)
				}
			}
			// Session-independent trigger fails after stock adjustment, exercising physical rollback.
			_, err = f.pool.Exec(f.ctx, `CREATE OR REPLACE FUNCTION transfer_test_fault() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.action IN ('dispatch','receipt') THEN RAISE EXCEPTION 'integration transfer fault'; END IF; RETURN NEW; END; $$; CREATE TRIGGER transfer_test_fault BEFORE INSERT ON stock_transfer_events FOR EACH ROW EXECUTE FUNCTION transfer_test_fault()`)
			if err != nil {
				t.Fatal(err)
			}
			t.Cleanup(func() {
				_, _ = f.pool.Exec(context.Background(), `DROP TRIGGER IF EXISTS transfer_test_fault ON stock_transfer_events; DROP FUNCTION IF EXISTS transfer_test_fault()`)
			})
			if receiptFailure {
				_, err = f.svc.TransferReceive(f.ctx, tr.ID, receipt(tr, "rollback-receipt", 5, 0, 0), false)
				f.balance(t, 15, 0)
			} else {
				_, err = f.svc.TransferApprove(f.ctx, tr.ID, f.actor)
				f.balance(t, 20, 0)
			}
			if err == nil {
				t.Fatal("fault did not abort transaction")
			}
			current, e := f.svc.TransferByID(f.ctx, tr.ID)
			if e != nil {
				t.Fatal(e)
			}
			if current.Status != tr.Status {
				t.Fatal("state escaped rollback")
			}
			if _, err = f.pool.Exec(f.ctx, `DROP TRIGGER transfer_test_fault ON stock_transfer_events; DROP FUNCTION transfer_test_fault()`); err != nil {
				t.Fatal(err)
			}
			if receiptFailure {
				_, err = f.svc.TransferReceive(f.ctx, tr.ID, receipt(tr, "rollback-receipt", 5, 0, 0), false)
				f.balance(t, 15, 5)
			} else {
				_, err = f.svc.TransferApprove(f.ctx, tr.ID, f.actor)
				f.balance(t, 15, 0)
			}
			if err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestTransferIntegrityDamagedQuarantineAndDraftEdit(t *testing.T) {
	f := transferSetup(t, "20")
	tr := f.draft(t, 5)
	edited, err := f.svc.TransferEdit(f.ctx, tr.ID, inventory.TransferInput{FromWarehouseID: f.from, ToWarehouseID: f.to, TransferDate: "2026-10-26", Items: []inventory.TransferItemInput{{ProductID: f.product, QuantityRequested: decimal.NewFromInt(4)}}}, false)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 20, 0)
	edited, err = f.svc.TransferApprove(f.ctx, edited.ID, f.actor)
	if err != nil {
		t.Fatal(err)
	}
	edited, err = f.svc.TransferReceive(f.ctx, edited.ID, receipt(edited, "damage", 3, 1, 0), false)
	if err != nil {
		t.Fatal(err)
	}
	f.balance(t, 16, 3)
	if !edited.Items[0].QuantityDamaged.Equal(decimal.NewFromInt(1)) || !edited.Items[0].QuantityTransit.IsZero() {
		t.Fatal("damaged quantity disappeared or entered available stock")
	}
	cancelled := f.draft(t, 2)
	if _, err = f.svc.TransferEdit(f.ctx, cancelled.ID, inventory.TransferInput{}, true); err != nil {
		t.Fatal(err)
	}
	f.balance(t, 16, 3)
	if _, err = f.svc.TransferApprove(f.ctx, cancelled.ID, f.actor); !errors.Is(err, inventory.ErrUnsupportedState) {
		t.Fatalf("cancel dispatch %v", err)
	}
}

func TestTransferIntegrityDestinationScopeAndConcurrentReceipt(t *testing.T) {
	f := transferSetup(t, "20")
	tr := f.draft(t, 5)
	var sourceStore, destinationStore int64
	for _, pair := range []struct {
		warehouse int64
		target    *int64
	}{{f.from, &sourceStore}, {f.to, &destinationStore}} {
		if err := f.pool.QueryRow(f.ctx, `INSERT INTO stores(code,name,warehouse_id) VALUES($1,'Transfer scope fixture',$2) RETURNING id`, unique("TRFS"), pair.warehouse).Scan(pair.target); err != nil {
			t.Fatal(err)
		}
	}
	sourceCtx := auth.WithStoreScope(f.ctx, auth.NewStoreScope([]int64{sourceStore}))
	destinationCtx := auth.WithStoreScope(f.ctx, auth.NewStoreScope([]int64{destinationStore}))
	if _, err := f.svc.TransferApprove(destinationCtx, tr.ID, f.actor); !errors.Is(err, inventory.ErrForbiddenWarehouse) {
		t.Fatalf("destination dispatched source: %v", err)
	}
	var err error
	tr, err = f.svc.TransferApprove(sourceCtx, tr.ID, f.actor)
	if err != nil {
		t.Fatal(err)
	}
	if _, err = f.svc.TransferReceive(sourceCtx, tr.ID, receipt(tr, "source-receipt", 5, 0, 0), false); !errors.Is(err, inventory.ErrForbiddenWarehouse) {
		t.Fatalf("source received destination: %v", err)
	}
	var wg sync.WaitGroup
	results := make(chan error, 2)
	for range 2 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, e := f.svc.TransferReceive(destinationCtx, tr.ID, receipt(tr, "concurrent-receipt", 5, 0, 0), false)
			results <- e
		}()
	}
	wg.Wait()
	close(results)
	for e := range results {
		if e != nil {
			t.Fatal(e)
		}
	}
	f.balance(t, 15, 5)
	if movementCount(t, f.pool) != 2 {
		t.Fatal("concurrent receipt duplicated movement")
	}
}
