//go:build !js && !(windows && (arm || 386)) && !(linux && (ppc64 || mips || mipsle || mips64))

package sqldb

import (
	"context"
	"database/sql"
	"fmt"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/lightningnetwork/lnd/sqldb/sqlc"
	"github.com/stretchr/testify/require"
)

// getInvoiceByHashForBench fetches an invoice by hash and reports any
// error except sql.ErrNoRows (expected when the invoice doesn't exist).
func getInvoiceByHashForBench(b *testing.B, store *SqliteStore,
	ctx context.Context, hash []byte) {

	_, err := store.GetInvoiceByHash(ctx, hash)
	if err != nil {
		require.ErrorIs(b, err, sql.ErrNoRows)
	}
}

// BenchmarkSqliteMaxConns benchmarks sequential reads against a SQLite
// database with varying MaxConnections settings.
//
// Run with:
//
//	go test -bench=BenchmarkSqliteMaxConns -benchmem -run=^$ ./sqldb/
func BenchmarkSqliteMaxConns(b *testing.B) {
	const numInvoices = 500

	// connCounts contains the MaxConnections values we want to compare.
	// 0 means "use the library default" (currently 2 for SQLite).
	connCounts := []int{1, 2, 4, 8, 16, 0}

	// Build a fresh SQLite database that will be shared across all
	// sub-benchmarks. We insert a fixed set of invoices once and then
	// execute read-only queries from multiple goroutines.
	dbFileName := filepath.Join(b.TempDir(), "bench.db")

	// Open the store once with migrations so the schema is in place.
	setupStore, err := NewSqliteStore(&SqliteConfig{
		SkipMigrations: false,
	}, dbFileName)
	require.NoError(b, err)

	require.NoError(b, setupStore.ApplyAllMigrations(
		context.Background(), GetMigrations(),
	))

	ctx := context.Background()

	// Insert test invoices. We use a predictable hash per invoice so we
	// can look them up deterministically during the benchmark.
	hashes := make([][]byte, numInvoices)
	for i := range numInvoices {
		hash := make([]byte, 32)
		hash[0] = byte(i)
		hash[1] = byte(i >> 8)
		hashes[i] = hash

		_, err := setupStore.InsertInvoice(
			ctx, sqlc.InsertInvoiceParams{
				Hash:               hash,
				PaymentAddr:        hash,
				PaymentRequestHash: hash,
				Expiry:             3600,
				CreatedAt:          time.Now(),
			},
		)
		require.NoError(b, err)
	}

	require.NoError(b, setupStore.DB.Close())

	for _, maxConns := range connCounts {
		name := fmt.Sprintf("MaxConns=%d", maxConns)
		if maxConns == 0 {
			name = fmt.Sprintf("MaxConns=default(%d)",
				DefaultSqliteMaxConns)
		}

		b.Run(name, func(b *testing.B) {
			store, err := NewSqliteStore(
				&SqliteConfig{
					SkipMigrations: true,
					MaxConnections: maxConns,
				}, dbFileName,
			)
			require.NoError(b, err)

			b.Cleanup(func() {
				require.NoError(b, store.DB.Close())
			})

			var i int
			for b.Loop() {
				hash := hashes[i%numInvoices]
				i++
				getInvoiceByHashForBench(b, store, ctx, hash)
			}
		})
	}
}

// BenchmarkSqliteMaxConnsConcurrentReads measures aggregate read throughput
// for a fixed level of goroutine concurrency to complement the sequential
// benchmark above. Each iteration launches a fixed number of goroutines that
// all issue reads simultaneously, directly stressing the connection pool.
func BenchmarkSqliteMaxConnsConcurrentReads(b *testing.B) {
	const (
		numInvoices = 500
		goroutines  = 16
	)

	connCounts := []int{1, 2, 4, 8, 16, 0}

	dbFileName := filepath.Join(b.TempDir(), "bench_conc.db")

	setupStore, err := NewSqliteStore(&SqliteConfig{
		SkipMigrations: false,
	}, dbFileName)
	require.NoError(b, err)

	require.NoError(b, setupStore.ApplyAllMigrations(
		context.Background(), GetMigrations(),
	))

	ctx := context.Background()

	hashes := make([][]byte, numInvoices)
	for i := range numInvoices {
		hash := make([]byte, 32)
		hash[0] = byte(i)
		hash[1] = byte(i >> 8)
		hashes[i] = hash

		_, err := setupStore.InsertInvoice(
			ctx, sqlc.InsertInvoiceParams{
				Hash:               hash,
				PaymentAddr:        hash,
				PaymentRequestHash: hash,
				Expiry:             3600,
				CreatedAt:          time.Now(),
			},
		)
		require.NoError(b, err)
	}

	require.NoError(b, setupStore.DB.Close())

	for _, maxConns := range connCounts {
		name := fmt.Sprintf("MaxConns=%d", maxConns)
		if maxConns == 0 {
			name = fmt.Sprintf("MaxConns=default(%d)",
				DefaultSqliteMaxConns)
		}

		b.Run(name, func(b *testing.B) {
			store, err := NewSqliteStore(
				&SqliteConfig{
					SkipMigrations: true,
					MaxConnections: maxConns,
				}, dbFileName,
			)
			require.NoError(b, err)

			b.Cleanup(func() {
				require.NoError(b, store.DB.Close())
			})

			for b.Loop() {
				var wg sync.WaitGroup
				wg.Add(goroutines)

				for g := range goroutines {
					hash := hashes[g%numInvoices]
					go func(h []byte) {
						defer wg.Done()
						getInvoiceByHashForBench(
							b, store, ctx, h,
						)
					}(hash)
				}

				wg.Wait()
			}
		})
	}
}
