package finance

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

	"github.com/shopspring/decimal"
)

var errSyncFailed = errors.New("sales order sync failed")

// payable builds the in-memory invoice PayInvoice locks: unpaid, still owed in
// full, and linked to a sales order so recordInvoicePayment runs the
// sales_orders sync.
func payable(f *fakeStore) *Invoice {
	inv := &Invoice{
		ID: 10, InvoiceNumber: "INV-1", SalesOrderID: 1, CustomerID: 1,
		InvoiceDate: "2026-08-11", DueDate: "2026-08-18", Status: "unpaid",
		TotalAmount: decimal.NewFromInt(250), RemainingAmount: decimal.NewFromInt(250),
	}
	f.invoices[inv.ID] = inv
	return inv
}

// A failed sales_orders sync must surface to the caller of recordInvoicePayment
// so the tx rolls back. The Exec result used to be discarded with `_, _ =`,
// which let PayInvoice commit: the invoice read as paid while the sales order
// kept a stale paid_amount and never reached 'paid'. The fake store applies
// writes immediately (no isolation), so the rollback evidence is the tx
// lifecycle itself — Commit must stay untouched, Rollback must be reached.
func TestPayInvoice_SalesOrderSyncFailureRollsBack(t *testing.T) {
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	fs := s.store.(*fakeStore)
	payable(fs)
	fs.execErr = errSyncFailed

	_, err := s.PayInvoice(context.Background(), 10, PaymentInput{Amount: decimal.NewFromInt(100), PaymentMethod: "transfer"})
	if !errors.Is(err, errSyncFailed) {
		t.Fatalf("PayInvoice error = %v, want %v", err, errSyncFailed)
	}
	if fs.commits != 0 {
		t.Errorf("commits = %d, want 0: a failed sync must never be committed", fs.commits)
	}
	if fs.rollbacks != 1 {
		t.Errorf("rollbacks = %d, want 1: the returned error must reach tx.Rollback", fs.rollbacks)
	}
	if len(fs.payments) != 0 {
		t.Errorf("payment rows = %d, want 0: no payment may be recorded for an unsynced order", len(fs.payments))
	}
}

// Same funnel, second caller: RecordPartialPayment must also abort instead of
// committing a partial payment against an unsynced sales order.
func TestRecordPartialPayment_SalesOrderSyncFailureRollsBack(t *testing.T) {
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	fs := s.store.(*fakeStore)
	payable(fs)

	// First payment succeeds and commits; capture the baseline.
	if _, err := s.PayInvoice(context.Background(), 10, PaymentInput{Amount: decimal.NewFromInt(100), PaymentMethod: "cash"}); err != nil {
		t.Fatal(err)
	}
	first := firstPaymentFor(t, fs, 10)
	commits, rollbacks := fs.commits, fs.rollbacks

	fs.execErr = errSyncFailed
	userID := int64(42)
	_, err := s.RecordPartialPayment(context.Background(), first.ID, &userID, PartialPaymentInput{
		Amount: decimal.NewFromInt(50), PaymentMethod: "transfer",
	})
	if !errors.Is(err, errSyncFailed) {
		t.Fatalf("RecordPartialPayment error = %v, want %v", err, errSyncFailed)
	}
	if fs.commits != commits {
		t.Errorf("commits = %d, want %d: the failing payment must not commit", fs.commits, commits)
	}
	if fs.rollbacks != rollbacks+1 {
		t.Errorf("rollbacks = %d, want %d: the returned error must reach tx.Rollback", fs.rollbacks, rollbacks+1)
	}
	if len(fs.payments) != 1 {
		t.Errorf("payment rows = %d, want 1 (only the first payment)", len(fs.payments))
	}
}

// Happy path unchanged: with a healthy sync the payment commits exactly as
// before. (The fake counts the deferred post-Commit Rollback, so only Commit is
// asserted here.)
func TestPayInvoice_SalesOrderSyncSuccessCommits(t *testing.T) {
	s := NewService(newFakeStore(), &fakeSeq{}, time.UTC)
	fs := s.store.(*fakeStore)
	payable(fs)

	inv, err := s.PayInvoice(context.Background(), 10, PaymentInput{Amount: decimal.NewFromInt(250), PaymentMethod: "transfer"})
	if err != nil {
		t.Fatalf("PayInvoice error = %v, want nil", err)
	}
	if inv.Status != "paid" || inv.RemainingAmount.String() != "0" {
		t.Errorf("invoice = status %s remaining %s, want paid/0", inv.Status, inv.RemainingAmount)
	}
	if fs.commits != 1 {
		t.Errorf("commits = %d, want 1", fs.commits)
	}
	if len(fs.payments) != 1 {
		t.Errorf("payment rows = %d, want 1", len(fs.payments))
	}
	// The sync statement must be issued exactly once for a linked order.
	if fs.execCalls != 1 {
		t.Errorf("raw Exec statements = %d, want 1", fs.execCalls)
	}
}
