package sales

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

	"github.com/jackc/pgx/v5/pgconn"
	"github.com/shopspring/decimal"
)

var errSnapSweepWrite = errors.New("snap sweep write failed")

// MarkInvoicePaid returns the rows it changed, so a caller can tell a settle
// from a no-op. The zero case is the whole point: a sweeper that settles
// against a stale candidate would otherwise write a payment row for an invoice
// nobody moved, and read "settled" from a statement that changed nothing.
//
// The count comes from the command tag, so a driver error must NOT be laundered
// into a count — the error path returns 0 with the error intact, and the caller
// must not read the 0 as "already settled".
func TestRepositoryMarkInvoicePaidRowCount(t *testing.T) {
	r := &Repository{}
	ctx := context.Background()
	cases := []struct {
		name    string
		tag     pgconn.CommandTag
		err     error
		want    int64
		wantErr error // errors.Is target; nil means no error
	}{
		{name: "settleable invoice changes one row", tag: pgconn.NewCommandTag("UPDATE 1"), want: 1},
		{name: "already paid changes no row", tag: pgconn.NewCommandTag("UPDATE 0"), want: 0},
		{name: "driver error surfaces with a zero count", tag: pgconn.NewCommandTag("UPDATE 0"), err: errSnapSweepWrite, want: 0, wantErr: errSnapSweepWrite},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := r.MarkInvoicePaid(ctx, &fakeTx{tag: tc.tag, err: tc.err}, 7, decimal.NewFromInt(200))
			if got != tc.want {
				t.Errorf("MarkInvoicePaid rows = %d, want %d", got, tc.want)
			}
			if tc.wantErr == nil {
				if err != nil {
					t.Errorf("MarkInvoicePaid error = %v, want nil", err)
				}
				return
			}
			if !errors.Is(err, tc.wantErr) {
				t.Errorf("MarkInvoicePaid error = %v, want %v", err, tc.wantErr)
			}
		})
	}
}

// A sweeper that read the throttle window off updated_at would never drain its
// candidate set: every probe would push the row's own age bound forward. The
// stamp is therefore its own column, and the statement must not touch
// updated_at at all.
func TestRepositorySetInvoiceSnapChecked_DoesNotTouchUpdatedAt(t *testing.T) {
	tx := &fakeTx{}
	if err := (&Repository{}).SetInvoiceSnapChecked(context.Background(), tx, 7); err != nil {
		t.Fatal(err)
	}
	stmt := strings.Join(strings.Fields(tx.lastSQL()), " ")
	if !strings.Contains(stmt, "snap_last_checked_at = now()") {
		t.Errorf("SetInvoiceSnapChecked does not stamp snap_last_checked_at: %s", stmt)
	}
	if strings.Contains(stmt, "updated_at") {
		t.Errorf("SetInvoiceSnapChecked touches updated_at, so a read-only probe would slide the candidate window forward: %s", stmt)
	}
}

// The stamp rides the caller's transaction and opens none of its own: the
// reconciler probes, then settles-or-backs-off in one transaction, and a
// rolled-back settle must roll the stamp back with it. A setter that committed
// independently would leave a stamp behind for a settle that never happened,
// and that invoice would sit unprobed until the throttle expired.
func TestRepositorySetInvoiceSnapChecked_UsesCallerTransactionOnly(t *testing.T) {
	tx := &fakeTx{}
	if err := (&Repository{}).SetInvoiceSnapChecked(context.Background(), tx, 7); err != nil {
		t.Fatal(err)
	}
	if len(tx.sql) != 1 {
		t.Fatalf("SetInvoiceSnapChecked issued %d statements, want exactly 1: %q", len(tx.sql), tx.sql)
	}
	if tx.commits != 0 {
		t.Errorf("SetInvoiceSnapChecked committed the caller's transaction itself (%d commits); the settle owns that boundary", tx.commits)
	}
	if tx.rollbacks != 0 {
		t.Errorf("SetInvoiceSnapChecked rolled the caller's transaction back (%d rollbacks)", tx.rollbacks)
	}
}

// A driver error must surface unchanged: a stamp that silently failed would
// leave the invoice unprobed-throttled and the sweeper would never retry it
// before the next pass.
func TestRepositorySetInvoiceSnapChecked_DriverErrorSurfaces(t *testing.T) {
	err := (&Repository{}).SetInvoiceSnapChecked(context.Background(), &fakeTx{err: errSnapSweepWrite}, 7)
	if !errors.Is(err, errSnapSweepWrite) {
		t.Errorf("SetInvoiceSnapChecked error = %v, want %v", err, errSnapSweepWrite)
	}
}
