// Copyright 2020 Tamás Gulácsi
//
//
// SPDX-License-Identifier: UPL-1.0 OR Apache-2.0

package godror_test

import (
	"context"
	"crypto/rand"
	"database/sql"
	"os"
	"os/exec"
	"strconv"
	"testing"
	"time"

	godror "github.com/godror/godror"
	"github.com/godror/godror/dsn"
	"github.com/oklog/ulid/v2"
	"golang.org/x/sync/errgroup"
)

func TestConcurrency(t *testing.T) {
	if os.Getenv("DO_TEST_CONCURRENCY") == "1" {
		testConcurrency(t)
		return
	}
	ex, err := os.Executable()
	if err != nil {
		t.Fatal(err)
	}
	// Run separately, in a separate process to continue with other tests on panic
	cmd := exec.Command(ex, "-test.run=TestConcurrency")
	cmd.Env = append(os.Environ(), "DO_TEST_CONCURRENCY=1")
	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	if err = cmd.Run(); err != nil {
		t.Fatal(err)
	}
}
func testConcurrency(t *testing.T) {
	ctx, cancel := context.WithTimeout(testContext("Concurrency"), 30*time.Second)
	defer cancel()

	P, err := dsn.Parse(testConStr)
	if err != nil {
		t.Fatal(err)
	}
	P.PoolParams.MinSessions = P.PoolParams.MaxSessions / 2
	P.PoolParams.WaitTimeout = 5 * time.Second
	db := sql.OpenDB(godror.NewConnector(P))
	defer db.Close()
	db.SetMaxIdleConns(0)

	// This limit is needed to avoid sigsegvs acquiring new connections
	if ok, _ := strconv.ParseBool(os.Getenv("GODROR_CONC_NO_LIMIT")); !ok {
		db.SetMaxOpenConns(P.PoolParams.MaxSessions)
	}

	const delQry = "DROP TABLE test_concurrency"
	db.ExecContext(ctx, delQry)
	qry := `CREATE TABLE test_concurrency (
      id         NUMBER(19) GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
      user_id    VARCHAR2(50) NOT NULL, 
      bId        VARCHAR2(50) UNIQUE NOT NULL, 
      actionType NUMBER(3) NOT NULL,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP   
    )`
	if _, err := db.ExecContext(ctx, qry); err != nil {
		t.Fatalf("%s: %+v", qry, err)
	}
	defer db.ExecContext(context.Background(), delQry)

	started := make(chan struct{})
	grp, grpCtx := errgroup.WithContext(ctx)
	printConnStats(t, db)
	start := time.Now()
	for i := 0; i < P.PoolParams.MaxSessions*128; i++ {
		i := i
		grp.Go(func() error {
			<-started
			return concHandleOne(grpCtx, db, i)
		})
	}
	close(started)
	printConnStats(t, db)
	done := make(chan struct{})
	defer close(done)
	go func() {
		for range time.Tick(10 * time.Second) {
			select {
			case <-done:
				return
			default:
				printConnStats(t, db)
			}
		}
	}()
	if err := grp.Wait(); err != nil {
		t.Fatal(err)
	}
	printConnStats(t, db)
	dur := time.Since(start)
	qry = "SELECT COUNT(0), MAX(created_at)-MIN(created_at) FROM test_concurrency"
	var n int
	var durS string
	if err := db.QueryRowContext(ctx, qry).Scan(&n, &durS); err != nil {
		t.Fatal(err)
	}
	t.Logf("Inserted %d rows in %s (%s)", n, durS, dur)
}
func concHandleOne(ctx context.Context, db *sql.DB, seq int) error {
	const qry = `INSERT INTO test_concurrency (bId, user_id, actionType) 
	  VALUES (:1, :2, :3)`
	ts := ulid.Timestamp(time.Now().UTC())
	bid := ulid.MustNew(ts, rand.Reader)
	uid := bid
	uid[15]++
	_, err := db.ExecContext(ctx, qry, bid, uid, 1)
	return err
}
func printConnStats(t testing.TB, db *sql.DB) {
	ctx, cancel := context.WithTimeout(testContext("poolStats"), time.Second)
	godror.Raw(ctx, db, func(c godror.Conn) error {
		poolStats, err := c.GetPoolStats()
		t.Logf("db: %+v: %s %v\n", db.Stats(), poolStats, err)
		return err
	})
	cancel()
}
