package integration

import (
	"context"
	"errors"
	"fmt"
	"github.com/TaruLab/keluargamart-backend/internal/finance"
	"github.com/TaruLab/keluargamart-backend/migrations"
	"github.com/jackc/pgx/v5/pgconn"
	"sync"
	"testing"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/cashier"
	"github.com/TaruLab/keluargamart-backend/internal/inventory"
	"github.com/TaruLab/keluargamart-backend/internal/sales"
	"github.com/shopspring/decimal"
)

func TestCashierIntegrity_LedgerClosureAndAuthority(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	uid := seedUser(t, pool)
	ctx := auth.ToContext(context.Background(), &auth.User{ID: uid, Role: auth.RoleCashier}, 1)
	repo := cashier.NewRepository(pool)
	svc := cashier.NewService(repo, seqAdapter{pool}, time.UTC)
	sh, err := svc.Open(ctx, cashier.OpenInput{CashierID: uid, OpeningCash: decimal.NewFromInt(10000)})
	if err != nil {
		t.Fatal(err)
	}
	note := "Rp8000 regression"
	spoof := int64(999999)
	entry, err := svc.CashIn(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(8000), Notes: &note, CreatedBy: &spoof})
	if err != nil {
		t.Fatal(err)
	}
	if entry.CreatedBy == nil || *entry.CreatedBy != uid {
		t.Fatalf("actor not authoritative: %+v", entry)
	}
	sum, err := svc.Summary(ctx, sh.ID)
	if err != nil {
		t.Fatal(err)
	}
	if !sum.CashIn.Equal(decimal.NewFromInt(8000)) || !sum.ExpectedCash.Equal(decimal.NewFromInt(18000)) || len(sum.Entries) != 1 || sum.Entries[0].ID != entry.ID {
		t.Fatalf("cash8000 summary: %+v", sum)
	}
	if _, err := svc.CashOut(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(3000)}); err != nil {
		t.Fatal(err)
	}
	for _, amount := range []int64{0, -1} {
		if _, err := svc.CashIn(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(amount)}); err == nil {
			t.Fatalf("accepted invalid %d", amount)
		}
	}
	if _, err := svc.CashIn(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.RequireFromString("0.001")}); err == nil {
		t.Fatal("accepted sub-cent entry")
	}
	if _, err := svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.RequireFromString("15000.001")}); err == nil {
		t.Fatal("accepted sub-cent count")
	}
	other := auth.ToContext(context.Background(), &auth.User{ID: uid + 1, Role: auth.RoleCashier}, 1)
	if _, err := svc.CashIn(other, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(1)}); !errors.Is(err, cashier.ErrForbidden) {
		t.Fatalf("other drawer: %v", err)
	}
	scoped := auth.WithStoreScope(ctx, auth.NewStoreScope([]int64{999}))
	if _, err := svc.Summary(scoped, sh.ID); !errors.Is(err, cashier.ErrNotFound) {
		t.Fatalf("unbound scope leaked: %v", err)
	}
	if _, err := svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(14999)}); !errors.Is(err, cashier.ErrCashMismatch) {
		t.Fatalf("normal mismatch: %v", err)
	}
	if _, err := svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(14999), Override: true, OverrideReason: "counted twice"}); !errors.Is(err, cashier.ErrForbidden) {
		t.Fatalf("cashier override: %v", err)
	}
	admin := auth.ToContext(context.Background(), &auth.User{ID: uid, Role: auth.RoleAdmin}, 1)
	if _, err := svc.Close(admin, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(14999), Override: true}); err == nil {
		t.Fatal("override without reason accepted")
	}
	closed, err := svc.Close(admin, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(14999), Override: true, OverrideReason: "counted twice"})
	if err != nil {
		t.Fatal(err)
	}
	if !closed.ExpectedCash.Equal(decimal.NewFromInt(15000)) || len(closed.ClosureAudit) != 1 {
		t.Fatalf("closed audit: %+v", closed)
	}
	a := closed.ClosureAudit[0]
	if !a.ExpectedCash.Equal(decimal.NewFromInt(15000)) || !a.ActualCash.Equal(decimal.NewFromInt(14999)) || !a.Difference.Equal(decimal.NewFromInt(-1)) || a.ActorID != uid || a.Reason != "counted twice" || !a.Override || a.CreatedAt.IsZero() {
		t.Fatalf("incomplete audit: %+v", a)
	}
	if _, err := svc.Close(admin, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(14999), Override: true, OverrideReason: "retry"}); !errors.Is(err, cashier.ErrShiftNotOpen) {
		t.Fatalf("duplicate close: %v", err)
	}
	var auditCount int
	if err := pool.QueryRow(ctx, `SELECT count(*) FROM cashier_shift_closures WHERE shift_id=$1`, sh.ID).Scan(&auditCount); err != nil || auditCount != 1 {
		t.Fatalf("audit once count=%d err=%v", auditCount, err)
	}
	if _, err := svc.CashIn(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(1)}); !errors.Is(err, cashier.ErrShiftNotOpen) {
		t.Fatalf("closed drawer: %v", err)
	}
	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback(ctx)
	if err := repo.RecordCashRefund(ctx, tx, sh.ID, decimal.NewFromInt(1), "late refund"); !errors.Is(err, cashier.ErrShiftNotOpen) {
		t.Fatalf("closed refund: %v", err)
	}
	if _, err := tx.Exec(ctx, `INSERT INTO cashier_drawer_entries(shift_id,type,amount) VALUES($1,'cash_in',1)`, sh.ID); err == nil {
		t.Fatal("database allowed post-close drawer insertion")
	}
}

func TestCashierIntegrity_CashOnlyRefundOnceAndCancelled(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	uid := seedUser(t, pool)
	ctx := auth.ToContext(context.Background(), &auth.User{ID: uid, Role: auth.RoleAdmin}, 1)
	repo := cashier.NewRepository(pool)
	svc := cashier.NewService(repo, seqAdapter{pool}, time.UTC)
	sh, err := svc.Open(ctx, cashier.OpenInput{CashierID: uid})
	if err != nil {
		t.Fatal(err)
	}
	wh := seedWarehouse(t, pool)
	p := seedProduct(t, pool, seedCategory(t, pool), seedUnit(t, pool), "100")
	seedStock(t, pool, p.ID, wh, "10")
	inv := inventory.NewService(inventory.NewRepository(pool), seqAdapter{pool}, wib)
	ss := salesService(pool, inv).WithShiftGuard(repo)
	customer := seedCustomer(t, pool)
	var cancelled int64
	for _, method := range []string{"cash", "transfer", "cash"} {
		so, _, err := ss.ProcessPOS(ctx, sales.POSInput{CustomerID: customer, WarehouseID: wh, ShiftID: &sh.ID, PaymentMethod: method, CashReceived: decimalPtr("100"), Items: []sales.POSItemInput{{ProductID: p.ID, Quantity: decimal.NewFromInt(1)}}})
		if err != nil {
			t.Fatal(err)
		}
		cancelled = so.ID
	}
	// Fixture cancellation is explicit; only remaining paid orders are receipts.
	if _, err := pool.Exec(ctx, `UPDATE sales_orders SET status='cancelled' WHERE id=$1`, cancelled); err != nil {
		t.Fatal(err)
	}
	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback(ctx)
	if err := repo.RecordCashRefund(ctx, tx, sh.ID, decimal.NewFromInt(20), "sales return #regression"); err != nil {
		t.Fatal(err)
	}
	if err := tx.Commit(ctx); err != nil {
		t.Fatal(err)
	}
	sum, err := svc.Summary(ctx, sh.ID)
	if err != nil {
		t.Fatal(err)
	}
	if !sum.CashSales.Equal(decimal.NewFromInt(100)) || !sum.NonCashSales.Equal(decimal.NewFromInt(100)) || !sum.CashOut.Equal(decimal.NewFromInt(20)) || !sum.ExpectedCash.Equal(decimal.NewFromInt(80)) {
		t.Fatalf("cash/noncash/refund/cancelled accounting: %+v", sum)
	}
	closed, err := svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.NewFromInt(80)})
	if err != nil {
		t.Fatal(err)
	}
	if !closed.ExpectedCash.Equal(sum.ExpectedCash) {
		t.Fatal("detail/close diverged")
	}
}

func TestCashierIntegrity_ConcurrentCloseAndCash(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	uid := seedUser(t, pool)
	ctx, cancel := context.WithTimeout(auth.ToContext(context.Background(), &auth.User{ID: uid, Role: auth.RoleCashier}, 1), 10*time.Second)
	defer cancel()
	svc := cashier.NewService(cashier.NewRepository(pool), seqAdapter{pool}, time.UTC)
	sh, err := svc.Open(ctx, cashier.OpenInput{CashierID: uid})
	if err != nil {
		t.Fatal(err)
	}
	start := make(chan struct{})
	var wg sync.WaitGroup
	var cashErr, closeErr error
	wg.Add(2)
	go func() {
		defer wg.Done()
		<-start
		_, cashErr = svc.CashIn(ctx, sh.ID, cashier.DrawerInput{Amount: decimal.NewFromInt(8000)})
	}()
	go func() {
		defer wg.Done()
		<-start
		_, closeErr = svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.Zero})
	}()
	close(start)
	wg.Wait()
	sum, err := svc.Summary(ctx, sh.ID)
	if err != nil {
		t.Fatal(err)
	}
	switch {
	case closeErr == nil:
		if !errors.Is(cashErr, cashier.ErrShiftNotOpen) || sum.Status != "closed" || !sum.ExpectedCash.IsZero() || len(sum.Entries) != 0 {
			t.Fatalf("close won but write leaked: cash=%v sum=%+v", cashErr, sum)
		}
	case cashErr == nil:
		if !errors.Is(closeErr, cashier.ErrCashMismatch) || sum.Status != "open" || !sum.ExpectedCash.Equal(decimal.NewFromInt(8000)) || len(sum.Entries) != 1 {
			t.Fatalf("cash won but close ignored it: close=%v sum=%+v", closeErr, sum)
		}
	default:
		t.Fatalf("neither serialized operation succeeded: cash=%v close=%v", cashErr, closeErr)
	}
}

func TestCashierMigration134_PopulatedUpgrade(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback(ctx)
	schema := fmt.Sprintf("cash134_%d", time.Now().UnixNano())
	if _, err := tx.Exec(ctx, `CREATE SCHEMA `+schema+`; SET LOCAL search_path TO `+schema); err != nil {
		t.Fatal(err)
	}
	_, err = tx.Exec(ctx, `
 CREATE TABLE users(id bigint PRIMARY KEY);
 CREATE TABLE cashier_shifts(id bigint PRIMARY KEY,status text,opening_cash numeric(15,2));
 CREATE TABLE cashier_drawer_entries(id bigserial PRIMARY KEY,shift_id bigint,type text,amount numeric(15,2));
 CREATE TABLE sales_orders(id bigint PRIMARY KEY,shift_id bigint,total_amount numeric,paid_amount numeric,status text);
 CREATE TABLE invoices(id bigint PRIMARY KEY,sales_order_id bigint);
 CREATE TABLE payments(id bigserial PRIMARY KEY,payable_type text,payable_id bigint,amount numeric,payment_method text);
 CREATE TABLE sales_returns(id bigserial PRIMARY KEY,sales_order_id bigint);
 INSERT INTO users VALUES(1);
 INSERT INTO cashier_shifts VALUES(1,'open',10000),(2,'closed',5000);
 INSERT INTO cashier_drawer_entries(shift_id,type,amount) VALUES(1,'cash_in',8000),(2,'cash_out',1000);
 INSERT INTO sales_orders VALUES(1,2,100,100,'paid');
 INSERT INTO invoices VALUES(1,1);
 INSERT INTO payments(payable_type,payable_id,amount,payment_method) VALUES(E'App\\Models\\Invoice',1,100,'cash');
 `)
	if err != nil {
		t.Fatal(err)
	}
	migration, err := migrations.FS.ReadFile("000134_pos_cash_integrity.sql")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := tx.Exec(ctx, string(migration)); err != nil {
		t.Fatal(err)
	}
	var n int
	var total decimal.Decimal
	if err := tx.QueryRow(ctx, `SELECT count(*),sum(amount) FROM cashier_drawer_entries`).Scan(&n, &total); err != nil {
		t.Fatal(err)
	}
	if n != 2 || !total.Equal(decimal.NewFromInt(9000)) {
		t.Fatalf("upgrade changed ledger n=%d total=%s", n, total)
	}
	if err := tx.QueryRow(ctx, `SELECT count(*) FROM cashier_shift_closures`).Scan(&n); err != nil || n != 0 {
		t.Fatalf("fabricated historical closure n=%d err=%v", n, err)
	}
	if _, err := tx.Exec(ctx, `INSERT INTO cashier_drawer_entries(shift_id,type,amount) VALUES(1,'cash_in',8000)`); err != nil {
		t.Fatal(err)
	}
	for _, statement := range []string{
		`INSERT INTO cashier_drawer_entries(shift_id,type,amount) VALUES(2,'cash_in',1)`,
		`INSERT INTO payments(payable_type,payable_id,amount,payment_method) VALUES(E'App\\Models\\Invoice',1,1,'cash')`,
		`INSERT INTO sales_returns(sales_order_id) VALUES(1)`,
		`UPDATE sales_orders SET status='cancelled' WHERE id=1`,
		`UPDATE cashier_drawer_entries SET amount=1 WHERE id=1`,
		`UPDATE payments SET payable_type='unbound',payable_id=999 WHERE id=1`,
		`UPDATE payments SET amount=1 WHERE id=1`,
	} {
		if _, err := tx.Exec(ctx, `SAVEPOINT rejected_write`); err != nil {
			t.Fatal(err)
		}
		if _, err := tx.Exec(ctx, statement); err == nil {
			t.Fatalf("closed financial mutation accepted: %s", statement)
		}
		if _, err := tx.Exec(ctx, `ROLLBACK TO SAVEPOINT rejected_write`); err != nil {
			t.Fatal(err)
		}
	}
}

func TestCashierIntegrity_FinancePaymentAfterCloseRollsBack(t *testing.T) {
	pool := testPool(t)
	setUp(t, pool)
	uid := seedUser(t, pool)
	ctx := auth.ToContext(context.Background(), &auth.User{ID: uid, Role: auth.RoleAdmin}, 1)
	svc := cashier.NewService(cashier.NewRepository(pool), seqAdapter{pool}, time.UTC)
	sh, err := svc.Open(ctx, cashier.OpenInput{CashierID: uid})
	if err != nil {
		t.Fatal(err)
	}
	customer := seedCustomer(t, pool)
	var orderID, invoiceID int64
	if err := pool.QueryRow(ctx, `INSERT INTO sales_orders(so_number,customer_id,order_date,status,shift_id,total_amount,remaining_amount) VALUES($1,$2,current_date,'invoiced',$3,100,100) RETURNING id`, unique("SO"), customer, sh.ID).Scan(&orderID); err != nil {
		t.Fatal(err)
	}
	if err := pool.QueryRow(ctx, `INSERT INTO invoices(invoice_number,sales_order_id,customer_id,invoice_date,due_date,status,total_amount,remaining_amount) VALUES($1,$2,$3,current_date,current_date,'unpaid',100,100) RETURNING id`, unique("INV"), orderID, customer).Scan(&invoiceID); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.Close(ctx, sh.ID, cashier.CloseInput{ActualCash: decimal.Zero}); err != nil {
		t.Fatal(err)
	}
	fsvc := finance.NewService(finance.NewRepository(pool), seqAdapter{pool}, time.UTC)
	_, err = fsvc.PayInvoice(ctx, invoiceID, finance.PaymentInput{Amount: decimal.NewFromInt(100), PaymentMethod: "cash"})
	var pe *pgconn.PgError
	if !errors.As(err, &pe) || pe.Code != "23514" || pe.ConstraintName != "cashier_shift_open" {
		t.Fatalf("finance closed guard error: %v", err)
	}
	var paid, remaining decimal.Decimal
	var status string
	if err := pool.QueryRow(ctx, `SELECT paid_amount,remaining_amount,status FROM invoices WHERE id=$1`, invoiceID).Scan(&paid, &remaining, &status); err != nil {
		t.Fatal(err)
	}
	if !paid.IsZero() || !remaining.Equal(decimal.NewFromInt(100)) || status != "unpaid" {
		t.Fatalf("partial invoice committed paid=%s remaining=%s status=%s", paid, remaining, status)
	}
	var payments int
	if err := pool.QueryRow(ctx, `SELECT count(*) FROM payments WHERE payable_id=$1`, invoiceID).Scan(&payments); err != nil || payments != 0 {
		t.Fatalf("late payment persisted count=%d err=%v", payments, err)
	}
}
