package storageaccess

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"fmt"
	"io"
	"reflect"
	"sync"
	"testing"
	"time"
)

var errStream = errors.New("stream interrupted")

type event struct {
	query string
	args  []driver.NamedValue
}
type state struct {
	mu                                sync.Mutex
	opens, closes, commits, rollbacks int
	events                            []event
}

func (s *state) record(q string, args []driver.NamedValue) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.events = append(s.events, event{q, append([]driver.NamedValue(nil), args...)})
}

type connector struct{ s *state }

func (c connector) Connect(ctx context.Context) (driver.Conn, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	c.s.mu.Lock()
	c.s.opens++
	c.s.mu.Unlock()
	return &connection{s: c.s}, nil
}
func (c connector) Driver() driver.Driver { return drv{c.s} }

type drv struct{ s *state }

func (d drv) Open(string) (driver.Conn, error) { return connector{d.s}.Connect(context.Background()) }

type connection struct{ s *state }

func (c *connection) Prepare(q string) (driver.Stmt, error) { return &statement{c, q}, nil }
func (c *connection) Close() error                          { c.s.mu.Lock(); defer c.s.mu.Unlock(); c.s.closes++; return nil }
func (c *connection) Begin() (driver.Tx, error) {
	return c.BeginTx(context.Background(), driver.TxOptions{})
}
func (c *connection) BeginTx(ctx context.Context, _ driver.TxOptions) (driver.Tx, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	return &transaction{c.s}, nil
}
func (c *connection) ExecContext(ctx context.Context, q string, a []driver.NamedValue) (driver.Result, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	c.s.record(q, a)
	return driver.RowsAffected(1), nil
}
func (c *connection) QueryContext(ctx context.Context, q string, a []driver.NamedValue) (driver.Rows, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	c.s.record(q, a)
	if q == "names" {
		return &rows{data: [][]driver.Value{{"Ada"}}, late: errStream}, nil
	}
	if q == "parents" {
		return &rows{data: [][]driver.Value{{int64(1)}, {int64(2)}, {int64(3)}}}, nil
	}
	return &rows{data: [][]driver.Value{{int64(1)}}}, nil
}

type transaction struct{ s *state }

func (t *transaction) Commit() error { t.s.mu.Lock(); defer t.s.mu.Unlock(); t.s.commits++; return nil }
func (t *transaction) Rollback() error {
	t.s.mu.Lock()
	defer t.s.mu.Unlock()
	t.s.rollbacks++
	return nil
}

type rows struct {
	data [][]driver.Value
	i    int
	late error
}

func (*rows) Columns() []string { return []string{"value"} }
func (*rows) Close() error      { return nil }
func (r *rows) Next(dest []driver.Value) error {
	if r.i < len(r.data) {
		copy(dest, r.data[r.i])
		r.i++
		return nil
	}
	if r.late != nil {
		return r.late
	}
	return io.EOF
}

type statement struct {
	c *connection
	q string
}

func (*statement) Close() error  { return nil }
func (*statement) NumInput() int { return -1 }
func (s *statement) Exec(a []driver.Value) (driver.Result, error) {
	return s.ExecContext(context.Background(), named(a))
}
func (s *statement) Query(a []driver.Value) (driver.Rows, error) {
	return s.QueryContext(context.Background(), named(a))
}
func (s *statement) ExecContext(ctx context.Context, a []driver.NamedValue) (driver.Result, error) {
	return s.c.ExecContext(ctx, s.q, a)
}
func (s *statement) QueryContext(ctx context.Context, a []driver.NamedValue) (driver.Rows, error) {
	return s.c.QueryContext(ctx, s.q, a)
}
func named(a []driver.Value) []driver.NamedValue {
	out := make([]driver.NamedValue, len(a))
	for i, v := range a {
		out[i] = driver.NamedValue{Ordinal: i + 1, Value: v}
	}
	return out
}
func openDB(t *testing.T) (*sql.DB, *state) {
	t.Helper()
	s := &state{}
	db := sql.OpenDB(connector{s})
	t.Cleanup(func() { db.Close() })
	return db, s
}
func TestPoolWaitAndRelease(t *testing.T) {
	db, _ := openDB(t)
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	held, err := db.Conn(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	_, err = db.ExecContext(ctx, "waiting")
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatal(err)
	}
	if err = held.Close(); err != nil {
		t.Fatal(err)
	}
	if _, err = db.ExecContext(context.Background(), "released"); err != nil {
		t.Fatal(err)
	}
	st := db.Stats()
	if st.WaitCount != 1 || st.InUse != 0 || st.Idle != 1 {
		t.Fatalf("%+v", st)
	}
	fmt.Printf("pool: timeout=%v wait=%d inuse=%d idle=%d\n", errors.Is(ctx.Err(), context.DeadlineExceeded), st.WaitCount, st.InUse, st.Idle)
}
func TestLifetime(t *testing.T) {
	db, _ := openDB(t)
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	db.SetConnMaxLifetime(time.Millisecond)
	held, err := db.Conn(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	time.Sleep(3 * time.Millisecond)
	if err = held.Close(); err != nil {
		t.Fatal(err)
	}
	if _, err = db.ExecContext(context.Background(), "new"); err != nil {
		t.Fatal(err)
	}
	if db.Stats().MaxLifetimeClosed < 1 {
		t.Fatal("expired connection not retired")
	}
	fmt.Println("lifetime: expired connection retired=true")
}
func TestTxLifecycle(t *testing.T) {
	db, s := openDB(t)
	ctx := context.Background()
	want := errors.New("business rejected")
	if err := InTx(ctx, db, func(tx *sql.Tx) error { _, e := tx.ExecContext(ctx, "change"); return e }); err != nil {
		t.Fatal(err)
	}
	if err := InTx(ctx, db, func(*sql.Tx) error { return want }); !errors.Is(err, want) {
		t.Fatal(err)
	}
	var done *sql.Tx
	if err := InTx(ctx, db, func(tx *sql.Tx) error { done = tx; return nil }); err != nil {
		t.Fatal(err)
	}
	if _, err := done.ExecContext(ctx, "too late"); !errors.Is(err, sql.ErrTxDone) {
		t.Fatal(err)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.commits != 2 || s.rollbacks != 1 {
		t.Fatal(s.commits, s.rollbacks)
	}
	fmt.Printf("tx: commits=%d rollbacks=%d reused-after-end=ErrTxDone\n", s.commits, s.rollbacks)
}
func TestRowsErrorAndNull(t *testing.T) {
	db, _ := openDB(t)
	names, err := Names(context.Background(), db)
	if names != nil || !errors.Is(err, errStream) || db.Stats().InUse != 0 {
		t.Fatal(names, err, db.Stats())
	}
	lateError := errors.Is(err, errStream)
	var s sql.NullString
	var n sql.Null[int64]
	if err = s.Scan(nil); err != nil {
		t.Fatal(err)
	}
	if err = n.Scan(int64(0)); err != nil {
		t.Fatal(err)
	}
	if s.Valid || !n.Valid || n.V != 0 {
		t.Fatal(s, n)
	}
	fmt.Printf("rows: late-error=%v inuse=%d; null-string=%v zero-valid=%v\n", lateError, db.Stats().InUse, s.Valid, n.Valid)
}
func TestPreparedKeepsValuesSeparate(t *testing.T) {
	db, s := openDB(t)
	ctx := context.Background()
	query := "SELECT id FROM users WHERE name = ?"
	value := "' OR 1=1 --"
	stmt, err := db.PrepareContext(ctx, query)
	if err != nil {
		t.Fatal(err)
	}
	defer stmt.Close()
	var id int
	if err = stmt.QueryRowContext(ctx, value).Scan(&id); err != nil {
		t.Fatal(err)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if len(s.events) != 1 || s.events[0].query != query || len(s.events[0].args) != 1 || s.events[0].args[0].Value != value {
		t.Fatal(s.events)
	}
	fmt.Printf("binding: query=%q value=%q\n", s.events[0].query, s.events[0].args[0].Value)
}
func TestNPlusOneCount(t *testing.T) {
	db, s := openDB(t)
	ctx := context.Background()
	getParents := func() []int {
		rr, err := db.QueryContext(ctx, "parents")
		if err != nil {
			t.Fatal(err)
		}
		defer rr.Close()
		var ids []int
		for rr.Next() {
			var id int
			if err := rr.Scan(&id); err != nil {
				t.Fatal(err)
			}
			ids = append(ids, id)
		}
		if err := rr.Err(); err != nil {
			t.Fatal(err)
		}
		return ids
	}
	ids := getParents()
	if !reflect.DeepEqual(ids, []int{1, 2, 3}) {
		t.Fatal(ids)
	}
	for _, id := range ids {
		var n int
		if err := db.QueryRowContext(ctx, "child", id).Scan(&n); err != nil {
			t.Fatal(err)
		}
	}
	s.mu.Lock()
	before := len(s.events)
	s.events = nil
	s.mu.Unlock()
	ids = getParents()
	var n int
	if err := db.QueryRowContext(ctx, "children for all parents", ids[0], ids[1], ids[2]).Scan(&n); err != nil {
		t.Fatal(err)
	}
	s.mu.Lock()
	after := len(s.events)
	s.mu.Unlock()
	if before != 4 || after != 2 {
		t.Fatal(before, after)
	}
	fmt.Printf("query-count: n+1=%d batched=%d\n", before, after)
}
