package parity

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

	"github.com/TaruLab/keluargamart-backend/internal/finance"
	"github.com/TaruLab/keluargamart-backend/internal/purchasing"
	"github.com/TaruLab/keluargamart-backend/internal/sales"
	"github.com/shopspring/decimal"
)

func loc() *time.Location { l, _ := time.LoadLocation("Asia/Jakarta"); return l }

type harness struct {
	d   *db
	seq *fakeSeq
	s   *sales.Service
	p   *purchasing.Service
	f   *finance.Service
}

func newHarness() *harness {
	d := newDB()
	seq := newFakeSeq()
	return &harness{
		d:   d,
		seq: seq,
		s:   sales.NewService(&salesStore{d}, seq, loc()),
		p:   purchasing.NewService(&purchStore{d}, seq, loc()),
		f:   finance.NewService(&financeStore{d}, seq, loc()),
	}
}

// TestRegularSO_Parity covers §2.1 (unrounded line math, header totals,
// reservation) + §2.4 confirm + §2.5 cancel (reservation released).
func TestRegularSO_Parity(t *testing.T) {
	h := newHarness()
	// Pricing engine is authoritative (Phase 6 item 2) — seed DB prices to match input UnitPrice for parity.
	h.d.prices[1] = decimal.NewFromInt(10000)
	h.d.prices[2] = decimal.NewFromInt(7500)
	ctx := context.Background()

	wh := int64(1)
	o, err := h.s.Create(ctx, sales.SOInput{
		CustomerID:     1,
		WarehouseID:    &wh,
		OrderDate:      "2026-08-11",
		TaxPercent:     decimal.NewFromInt(10),
		DiscountAmount: decimal.NewFromInt(5000),
		ShippingCost:   decimal.NewFromInt(2000),
		Items: []sales.SOItemInput{
			{ProductID: 1, Quantity: decimal.NewFromInt(2), UnitPrice: decimal.NewFromInt(10000), DiscountPercent: decimal.NewFromInt(5)},
			{ProductID: 2, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(7500)},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	// §2.1.4: line discount = 10000*2*0.05 = 1000, sub = 19000; +7500 => 26500
	if o.Subtotal.String() != "26500" {
		t.Errorf("subtotal = %s, want 26500", o.Subtotal)
	}
	// §2.1.5: tax unrounded = 2650
	if o.TaxAmount.String() != "2650" {
		t.Errorf("tax = %s, want 2650", o.TaxAmount)
	}
	// §2.1.6: total = 26500+2650-5000+2000
	if o.TotalAmount.String() != "26150" || o.RemainingAmount.String() != "26150" {
		t.Errorf("total/remaining = %s/%s, want 26150", o.TotalAmount, o.RemainingAmount)
	}
	if o.Status != "draft" || o.OrderType != "regular" {
		t.Errorf("status/type = %s/%s, want draft/regular", o.Status, o.OrderType)
	}
	// §2.1.8: reserved
	if got := h.d.reservedQty(1, 1); got.String() != "2" {
		t.Errorf("reserved p1 = %s, want 2", got)
	}
	if got := h.d.reservedQty(2, 1); got.String() != "1" {
		t.Errorf("reserved p2 = %s, want 1", got)
	}

	// §2.4 confirm: draft -> confirmed, reservation unchanged
	o, err = h.s.Confirm(ctx, o.ID)
	if err != nil || o.Status != "confirmed" {
		t.Fatalf("confirm = %v/%s", err, o.Status)
	}
	if got := h.d.reservedQty(1, 1); got.String() != "2" {
		t.Errorf("reserved p1 after confirm = %s, want 2", got)
	}

	// §2.5 cancel: cannot cancel delivered/cancelled, releases reservation
	if _, err := h.s.Cancel(ctx, o.ID); err != nil {
		t.Fatal(err)
	}
	if got := h.d.reservedQty(1, 1); got.String() != "0" {
		t.Errorf("reserved p1 after cancel = %s, want 0", got)
	}
	if _, err := h.s.Cancel(ctx, o.ID); !errors.Is(err, sales.ErrCannotCancel) {
		t.Errorf("second cancel = %v, want ErrCannotCancel", err)
	}
}

// TestPOS_Parity covers §2.2 + §16.1: stock deducted (server price), SO
// invoiced, auto-invoice + payment at check-out; then §5.2 pay closes it
// via the finance module. Negative stock is rejected by default (§11.3); the
// permissive legacy path is covered by TestPOS_NegativeStock_WhenEnabled.
func TestPOS_Parity(t *testing.T) {
	h := newHarness()
	ctx := context.Background()
	h.d.setStock(1, 1, decimal.NewFromInt(10))

	o, _, err := h.s.ProcessPOS(ctx, sales.POSInput{
		CustomerID:    1,
		WarehouseID:   1,
		PaymentMethod: "cash",
		Items:         []sales.POSItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(3), UnitPrice: decimal.NewFromInt(5000)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	// §2.2.6 + §16.1: server price snapshot 100 x 3 = 300; stock 10 -> 7
	if got := h.d.stockQty(1, 1); got.String() != "7" {
		t.Errorf("stock p1 = %s, want 7", got)
	}
	if o.TotalAmount.String() != "300" {
		t.Errorf("total = %s, want 300 (server price 100 x 3)", o.TotalAmount)
	}
	// §2.2.3/4: subtotal = total = 300; invoice fully paid at POS so SO remaining is 0
	if o.RemainingAmount.String() != "0" {
		t.Errorf("remaining = %s, want 0", o.RemainingAmount)
	}
	// §2.2.8: SO invoiced + auto invoice exists (paid)
	if o.Status != "invoiced" {
		t.Errorf("SO status = %s, want invoiced", o.Status)
	}
	if o.OrderType != "pos" {
		t.Errorf("order_type = %s, want pos", o.OrderType)
	}
	if len(h.d.invoices) != 1 {
		t.Fatalf("invoices = %d, want 1", len(h.d.invoices))
	}
	var inv *finance.Invoice
	for _, i := range h.d.invoices {
		inv = i
	}
	if inv.Status != "paid" || inv.PaidAmount.String() != "300" || inv.RemainingAmount.String() != "0" {
		t.Errorf("invoice = %s/paid %s/remaining %s, want paid/300/0", inv.Status, inv.PaidAmount, inv.RemainingAmount)
	}
	// Flow-audit bug 2: POS till sales are settled immediately (cash/qris),
	// so due date equals invoice date (terms = 0).
	wantDue := inv.InvoiceDate
	if inv.DueDate != wantDue {
		t.Errorf("due = %s, want %s", inv.DueDate, wantDue)
	}
	// §2.3 POS records a payment at check-out; cashflows are manual.
	if len(h.d.payments) != 1 {
		t.Fatalf("payments = %d, want 1", len(h.d.payments))
	}
	foundPay := false
	for _, p := range h.d.payments {
		if p.PayableID == inv.ID && p.PayableType == finance.InvoiceModel && p.PaymentNumber[:3] == "PAY" && p.Amount.String() == "300" && p.PaymentMethod == "cash" {
			foundPay = true
		}
	}
	if !foundPay {
		t.Error("pos payment not recorded against invoice")
	}
}

// TestPOS_NegativeStock_WhenEnabled verifies the permissive Laravel-compatible
// path (negative stock) is preserved only behind the AllowNegativeStock flag
// (§11.3 confirmed rule supersedes it by default).
func TestPOS_NegativeStock_WhenEnabled(t *testing.T) {
	h := newHarness()
	ctx := context.Background()
	h.s.WithAllowNegativeStock(true)
	h.d.setStock(1, 1, decimal.Zero)

	o, _, err := h.s.ProcessPOS(ctx, sales.POSInput{
		CustomerID:    1,
		WarehouseID:   1,
		PaymentMethod: "cash",
		Items:         []sales.POSItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(3)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != "invoiced" {
		t.Errorf("status = %s, want invoiced", o.Status)
	}
	if got := h.d.stockQty(1, 1); got.String() != "-3" {
		t.Errorf("stock p1 = %s, want -3 (negative allowed behind flag)", got)
	}
}

// TestPOCreate_Parity covers §4.1: line subtotal, integer-rounded tax
// (PHP round half-up), and the max(0,...) total clamp.
func TestPOCreate_Parity(t *testing.T) {
	h := newHarness()
	ctx := context.Background()

	o, err := h.p.Create(ctx, purchasing.POInput{
		SupplierID:  1,
		WarehouseID: 1,
		OrderDate:   "2026-08-11",
		TaxPercent:  decimal.NewFromInt(10),
		Items: []purchasing.POItemInput{
			{ProductID: 1, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.RequireFromString("1234.50")},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	// §4.1.3/4: subtotal 1234.50
	if o.Subtotal.String() != "1234.5" {
		t.Errorf("subtotal = %s, want 1234.5", o.Subtotal)
	}
	// §4.1.5: tax = round(123.45) = 123 (integer, half-up)
	if o.TaxAmount.String() != "123" {
		t.Errorf("tax = %s, want 123", o.TaxAmount)
	}
	// §4.1.6: total = 1234.50 + 123 = 1357.50
	if o.TotalAmount.String() != "1357.5" || o.RemainingAmount.String() != "1357.5" {
		t.Errorf("total/remaining = %s/%s, want 1357.5", o.TotalAmount, o.RemainingAmount)
	}

	// §4.1.6 clamp: discount > subtotal+tax => total 0
	clamped, err := h.p.Create(ctx, purchasing.POInput{
		SupplierID: 1, WarehouseID: 1, OrderDate: "2026-08-11",
		TaxPercent: decimal.NewFromInt(10), DiscountAmount: decimal.NewFromInt(5000),
		Items: []purchasing.POItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(100)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if clamped.TotalAmount.String() != "0" {
		t.Errorf("clamped total = %s, want 0", clamped.TotalAmount)
	}
}

// TestPOGR_Parity covers §4.2 approve + §4.3 GR (raw stock increment, no
// movement, PO -> received).
func TestPOGR_Parity(t *testing.T) {
	h := newHarness()
	ctx := context.Background()

	o, err := h.p.Create(ctx, purchasing.POInput{
		SupplierID: 1, WarehouseID: 1, OrderDate: "2026-08-11",
		TaxPercent: decimal.NewFromInt(10),
		Items:      []purchasing.POItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(3), UnitPrice: decimal.NewFromInt(1000)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	// §4.2 approve
	if _, err := h.p.Approve(ctx, o.ID, 1); err != nil {
		t.Fatal(err)
	}
	if h.d.pos[o.ID].Status != "approved" {
		t.Errorf("PO status = %s, want approved", h.d.pos[o.ID].Status)
	}

	// §4.3 GR full receipt
	g, err := h.p.CreateGR(ctx, purchasing.GRInput{
		PurchaseOrderID: o.ID,
		WarehouseID:     1,
		ReceivedDate:    "2026-08-12",
		Items: []purchasing.GRItemInput{
			{ProductID: 1, QuantityReceived: decimal.NewFromInt(3)},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	if g.Status != "confirmed" {
		t.Errorf("GR status = %s, want confirmed", g.Status)
	}
	// §4.3.6: full receipt => received
	if h.d.pos[o.ID].Status != "received" {
		t.Errorf("PO status = %s, want received", h.d.pos[o.ID].Status)
	}
	// §4.3.4: raw stock +3 (no movement recorded)
	if got := h.d.stockQty(1, 1); got.String() != "3" {
		t.Errorf("stock p1 = %s, want 3", got)
	}
	if len(h.d.poIt[o.ID]) != 1 || h.d.poIt[o.ID][0].ReceivedQuantity.String() != "3" {
		t.Errorf("received_quantity not bumped: %+v", h.d.poIt[o.ID])
	}
}

// TestPayDebt_Parity covers §4.5.4: debt settlement flips the PO to paid with
// remaining 0. Cashflow entries are manual (Arus Kas Kasir), none generated.
func TestPayDebt_Parity(t *testing.T) {
	h := newHarness()
	ctx := context.Background()

	o, err := h.p.Create(ctx, purchasing.POInput{
		SupplierID: 1, WarehouseID: 1, OrderDate: "2026-08-11",
		TaxPercent: decimal.NewFromInt(10),
		Items:      []purchasing.POItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(3), UnitPrice: decimal.NewFromInt(1000)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	// force received so it's payable
	h.d.pos[o.ID].Status = "received"

	po, err := h.p.PayDebt(ctx, o.ID, decimal.NewFromInt(3300), nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	// §4.5.4 — remaining settles to 0; the lifecycle status ('received') is
	// kept now that payment no longer overwrites goods state (2026-09).
	if po.Status != "received" || po.RemainingAmount.String() != "0" {
		t.Errorf("status/remaining = %s/%s, want received/0", po.Status, po.RemainingAmount)
	}
}

// TestInvoicePay_Parity covers §5.1 (invoice from confirmed SO, subtotal/tax
// left 0.00) and §5.2 (partial then full payment).
func TestInvoicePay_Parity(t *testing.T) {
	h := newHarness()
	h.d.prices[1] = decimal.NewFromInt(10000)
	ctx := context.Background()

	wh := int64(1)
	o, err := h.s.Create(ctx, sales.SOInput{
		CustomerID: 1, WarehouseID: &wh, OrderDate: "2026-08-11",
		Items: []sales.SOItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(10000)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := h.s.Confirm(ctx, o.ID); err != nil {
		t.Fatal(err)
	}

	inv, err := h.f.CreateInvoice(ctx, o.ID)
	if err != nil {
		t.Fatal(err)
	}
	// §5.1.3: subtotal/tax left 0.00, total from SO
	if inv.Subtotal.String() != "0" || inv.TaxAmount.String() != "0" || inv.TotalAmount.String() != "10000" {
		t.Errorf("sub/tax/total = %s/%s/%s, want 0/0/10000", inv.Subtotal, inv.TaxAmount, inv.TotalAmount)
	}
	if inv.Status != "unpaid" || inv.RemainingAmount.String() != "10000" {
		t.Errorf("status/remaining = %s/%s, want unpaid/10000", inv.Status, inv.RemainingAmount)
	}
	// §5.1.2 cash customer = due today; §5.1.5 SO invoiced
	wantDue, _ := time.Parse("2006-01-02", inv.InvoiceDate)
	if inv.DueDate != wantDue.Format("2006-01-02") {
		t.Errorf("due = %s, want %s", inv.DueDate, wantDue.Format("2006-01-02"))
	}
	if h.d.orders[o.ID].Status != "invoiced" {
		t.Errorf("SO status = %s, want invoiced", h.d.orders[o.ID].Status)
	}

	// §5.2 partial
	inv, err = h.f.PayInvoice(ctx, inv.ID, finance.PaymentInput{Amount: decimal.NewFromInt(4000), PaymentMethod: "transfer"})
	if err != nil {
		t.Fatal(err)
	}
	if inv.Status != "partial" || inv.PaidAmount.String() != "4000" || inv.RemainingAmount.String() != "6000" {
		t.Errorf("partial = %s/%s/%s, want partial/4000/6000", inv.Status, inv.PaidAmount, inv.RemainingAmount)
	}
	// §5.2 full
	inv, err = h.f.PayInvoice(ctx, inv.ID, finance.PaymentInput{Amount: decimal.NewFromInt(6000), PaymentMethod: "cash"})
	if err != nil {
		t.Fatal(err)
	}
	if inv.Status != "paid" || inv.RemainingAmount.String() != "0" {
		t.Errorf("final = %s/%s, want paid/0", inv.Status, inv.RemainingAmount)
	}
	if len(h.d.payments) != 2 {
		t.Errorf("payments = %d, want 2", len(h.d.payments))
	}
}

// TestVoucherPayDebt covers §4.5.5/§4.5.7 voucher consumption in payDebt.
func TestVoucherPayDebt(t *testing.T) {
	h := newHarness()
	ctx := context.Background()

	o, err := h.p.Create(ctx, purchasing.POInput{
		SupplierID: 1, WarehouseID: 1, OrderDate: "2026-08-11",
		Items: []purchasing.POItemInput{{ProductID: 1, Quantity: decimal.NewFromInt(1), UnitPrice: decimal.NewFromInt(1000)}},
	})
	if err != nil {
		t.Fatal(err)
	}
	h.d.pos[o.ID].Status = "received"

	v, err := h.p.CreateVoucher(ctx, purchasing.VoucherInput{VoucherDate: "2026-08-11", Amount: decimal.NewFromInt(1000)})
	if err != nil {
		t.Fatal(err)
	}
	if v.Status != "available" || v.Type != "payment" {
		t.Errorf("voucher = %s/%s, want available/payment", v.Status, v.Type)
	}
	if v.VoucherNumber[:2] != "PV" {
		t.Errorf("voucher number = %s, want PV prefix", v.VoucherNumber)
	}

	po, err := h.p.PayDebt(ctx, o.ID, decimal.NewFromInt(1000), &v.ID, nil)
	if err != nil {
		t.Fatal(err)
	}
	// Payment no longer overwrites the goods lifecycle (2026-09): a paid
	// received PO stays 'received'; remaining_amount carries the debt state.
	if po.Status != "received" {
		t.Errorf("PO status = %s, want received (lifecycle kept)", po.Status)
	}
	// §4.5.5/7: voucher used
	if h.d.vouchers[v.ID].Status != "used" {
		t.Errorf("voucher status = %s, want used", h.d.vouchers[v.ID].Status)
	}
}
