package sales

import (
	"context"
	"os"
	"strings"
	"testing"

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

func repobits(n int64) decimal.Decimal { return decimal.NewFromInt(n) }

// unroutablePool builds a REAL pool that cannot connect.
//
// A fake store cannot catch the bug this file guards: the defect is in how the
// method REACHES the database, so a fake never exercises it. A real pool does,
// and it is cheap — pgxpool.New does not dial, it only parses the URL and starts
// the health-check goroutine. Pointed at 127.0.0.1:1, every operation returns a
// connection error IMMEDIATELY (nothing listens on port 1), so the test needs no
// database and no timeout.
//
// That is what makes the assertion meaningful: before the fix these methods
// panicked on the nil interface; now they return a driver error. "Panic" versus
// "error" is the entire contract, and only a real pool distinguishes them —
// a Repository with a nil pool panics either way, which is what the first
// version of this test wrongly asserted.

// Every hand-written statement in repository.go takes the same `q db.DBTX` as
// the generated ones, and `nil` means "the caller is not in a transaction, use
// the pool" — the same meaning it has in queries(). Eight of those methods were
// calling q.Exec / q.QueryRow on the argument directly. A nil interface has no
// methods, so each panicked instead of running.
//
// This was not theoretical. ProcessPOS reaches SetInvoiceSnapRef with a literal
// nil (service.go), and every QRIS sale died with a 500 at that line: the
// deployed POS could not take a QRIS payment at all. It compiled, vetted and
// passed the whole suite, because every unit test in this package drives a fake
// store and never constructs a *Repository backed by a pool.
func TestRepositoryInvoiceWritersSurviveANilDBTX(t *testing.T) {
	pool, err := pgxpool.New(context.Background(), "postgres://u:p@127.0.0.1:1/db?connect_timeout=1&sslmode=disable")
	if err != nil {
		t.Fatalf("could not build the stand-in pool: %v", err)
	}
	defer pool.Close()

	// This is the fix in one line: a nil DBTX must resolve to the pool, never be
	// passed through to a method call.
	r := &Repository{pool: pool, q: db.New(pool)}
	if got := r.execer(nil); got == nil {
		t.Fatal("execer(nil) = nil, want the repository's pool")
	} else if got != db.DBTX(pool) {
		t.Error("execer(nil) did not return the repository's own pool")
	}

	ctx := context.Background()
	cases := []struct {
		name string
		call func() error
	}{
		{"SetInvoiceSnapRef", func() error { return r.SetInvoiceSnapRef(ctx, nil, 1, "ref") }},
		{"SetInvoiceSnapChecked", func() error { return r.SetInvoiceSnapChecked(ctx, nil, 1) }},
		{"MarkInvoicePaid", func() error { _, err := r.MarkInvoicePaid(ctx, nil, 1, repobits(100)); return err }},
		{"SettleOrderPayment", func() error { return r.SettleOrderPayment(ctx, nil, 1, repobits(100)) }},
		{"CancelOrderInvoices", func() error { return r.CancelOrderInvoices(ctx, nil, 1) }},
		{"OpenInvoiceForOrder", func() error { _, err := r.OpenInvoiceForOrder(ctx, nil, 1); return err }},
		{"InvoiceForOrder", func() error { _, err := r.InvoiceForOrder(ctx, nil, 1); return err }},
		{"InvoiceBySnapRef", func() error { _, err := r.InvoiceBySnapRef(ctx, nil, "ref"); return err }},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			defer func() {
				if rec := recover(); rec != nil {
					t.Fatalf("%s panicked on a nil DBTX: %v", tc.name, rec)
				}
			}()
			// The error value is deliberately not asserted: nothing is listening
			// on port 1, so the content depends on the driver. "Returns instead
			// of panicking" is the contract under test.
			_ = tc.call()
		})
	}
}

// Guard against the class returning. A hand-written statement that touches the
// caller's DBTX directly is a latent nil panic, and execer() exists precisely so
// that never has to happen. This is a textual check because the failure is a nil
// dereference on a line no unit test can reach without a live database.
func TestNoRepositoryMethodCallsTheCallersDBTXDirectly(t *testing.T) {
	statements := []string{
		"OpenInvoiceForOrder", "InvoiceForOrder", "MarkInvoicePaid",
		"SettleOrderPayment", "SetInvoiceSnapRef", "SetInvoiceSnapChecked",
		"InvoiceBySnapRef", "CancelOrderInvoices",
	}
	src, err := os.ReadFile("repository.go")
	if err != nil {
		t.Fatal(err)
	}
	for _, name := range statements {
		body := methodBody(string(src), "func (r *Repository) "+name+"(")
		if body == "" {
			t.Errorf("method %s not found in repository.go — this guard is no longer covering it", name)
			continue
		}
		for _, verb := range []string{"Exec", "QueryRow", "Query"} {
			if strings.Contains(body, "q."+verb+"(") {
				t.Errorf("%s calls q.%s directly; route it through r.execer(q) so a nil "+
					"DBTX falls back to the pool instead of panicking", name, verb)
			}
		}
	}
}

// methodBody returns the source of the method whose signature starts with
// header, up to the closing brace at column 0. gofmt puts a top-level closing
// brace in the first column, which is the only reliable end marker without
// parsing the file.
func methodBody(src, header string) string {
	i := strings.Index(src, header)
	if i < 0 {
		return ""
	}
	rest := src[i+len(header):]
	end := strings.Index(rest, "\n}\n")
	if end < 0 {
		return ""
	}
	return rest[:end]
}
