package coredb

import (
	"database/sql"
	"sync"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"
)

func TestNewSQLDBPoolControllerAppliesInitialConfig(t *testing.T) {
	t.Parallel()

	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	t.Cleanup(func() { _ = db.Close() })

	controller, err := NewSQLDBPoolController(db, DBPoolConfig{
		MaxOpenConns:    11,
		MaxIdleConns:    7,
		ConnMaxLifetime: 5 * time.Minute,
		ConnMaxIdleTime: 2 * time.Minute,
	})
	require.NoError(t, err)

	snapshot := controller.Snapshot()
	require.Equal(t, 11, snapshot.Config.MaxOpenConns)
	require.Equal(t, 7, snapshot.Config.MaxIdleConns)
	require.Equal(t, 5*time.Minute, snapshot.Config.ConnMaxLifetime)
	require.Equal(t, 2*time.Minute, snapshot.Config.ConnMaxIdleTime)
	require.Equal(t, 11, snapshot.Stats.MaxOpenConnections)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestSQLDBPoolControllerUpdateChangesEffectiveConfig(t *testing.T) {
	t.Parallel()

	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	t.Cleanup(func() { _ = db.Close() })

	controller, err := NewSQLDBPoolController(db, DBPoolConfig{MaxOpenConns: 5, MaxIdleConns: 2})
	require.NoError(t, err)

	snapshot, err := controller.Update(DBPoolConfig{
		MaxOpenConns:    19,
		MaxIdleConns:    17,
		ConnMaxLifetime: time.Minute,
		ConnMaxIdleTime: 30 * time.Second,
	})
	require.NoError(t, err)
	require.Equal(t, 19, snapshot.Config.MaxOpenConns)
	require.Equal(t, 17, snapshot.Config.MaxIdleConns)
	require.Equal(t, time.Minute, snapshot.Config.ConnMaxLifetime)
	require.Equal(t, 30*time.Second, snapshot.Config.ConnMaxIdleTime)
	require.Equal(t, 19, snapshot.Stats.MaxOpenConnections)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestSQLDBPoolControllerSnapshotWaitsForInFlightUpdate(t *testing.T) {
	t.Parallel()

	pool := &blockingDBPool{
		stats:          sql.DBStats{MaxOpenConnections: 5},
		maxOpenStarted: make(chan struct{}),
		releaseMaxOpen: make(chan struct{}),
		statsCalled:    make(chan struct{}, 1),
	}
	controller := &SQLDBPoolController{
		db:     pool,
		config: DBPoolConfig{MaxOpenConns: 5, MaxIdleConns: 2},
	}

	updateErr := make(chan error, 1)
	go func() {
		_, err := controller.Update(DBPoolConfig{MaxOpenConns: 9, MaxIdleConns: 4})
		updateErr <- err
	}()

	select {
	case <-pool.maxOpenStarted:
	case <-time.After(time.Second):
		t.Fatal("Update did not reach SetMaxOpenConns")
	}

	snapshotDone := make(chan DBPoolSnapshot, 1)
	go func() {
		snapshotDone <- controller.Snapshot()
	}()

	select {
	case <-pool.statsCalled:
		t.Fatal("Snapshot read stats while Update was applying pool setters")
	case snapshot := <-snapshotDone:
		t.Fatalf("Snapshot returned during an in-flight Update: %+v", snapshot)
	case <-time.After(25 * time.Millisecond):
	}

	close(pool.releaseMaxOpen)
	require.NoError(t, <-updateErr)

	snapshot := <-snapshotDone
	require.Equal(t, 9, snapshot.Config.MaxOpenConns)
	require.Equal(t, 9, snapshot.Stats.MaxOpenConnections)
}

func TestSQLDBPoolControllerRejectsInvalidConfig(t *testing.T) {
	t.Parallel()

	db, mock, err := sqlmock.New()
	require.NoError(t, err)
	t.Cleanup(func() { _ = db.Close() })

	controller, err := NewSQLDBPoolController(db, DBPoolConfig{MaxOpenConns: 5, MaxIdleConns: 5})
	require.NoError(t, err)

	_, err = controller.Update(DBPoolConfig{MaxOpenConns: 3, MaxIdleConns: 4})
	require.EqualError(t, err, "maxIdleConns must be less than or equal to maxOpenConns when maxOpenConns is set")
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestValidateDBPoolConfigRejectsNegativeValues(t *testing.T) {
	t.Parallel()

	err := ValidateDBPoolConfig(DBPoolConfig{MaxOpenConns: -1})
	require.EqualError(t, err, "maxOpenConns must be non-negative")

	err = ValidateDBPoolConfig(DBPoolConfig{MaxIdleConns: -1})
	require.EqualError(t, err, "maxIdleConns must be non-negative")

	err = ValidateDBPoolConfig(DBPoolConfig{ConnMaxLifetime: -time.Second})
	require.EqualError(t, err, "connMaxLifetime must be non-negative")

	err = ValidateDBPoolConfig(DBPoolConfig{ConnMaxIdleTime: -time.Second})
	require.EqualError(t, err, "connMaxIdleTime must be non-negative")
}

type blockingDBPool struct {
	mu             sync.Mutex
	stats          sql.DBStats
	maxOpenStarted chan struct{}
	releaseMaxOpen chan struct{}
	statsCalled    chan struct{}
}

func (pool *blockingDBPool) SetMaxOpenConns(n int) {
	close(pool.maxOpenStarted)
	<-pool.releaseMaxOpen

	pool.mu.Lock()
	pool.stats.MaxOpenConnections = n
	pool.mu.Unlock()
}

func (pool *blockingDBPool) SetMaxIdleConns(int) {}

func (pool *blockingDBPool) SetConnMaxLifetime(time.Duration) {}

func (pool *blockingDBPool) SetConnMaxIdleTime(time.Duration) {}

func (pool *blockingDBPool) Stats() sql.DBStats {
	select {
	case pool.statsCalled <- struct{}{}:
	default:
	}

	pool.mu.Lock()
	defer pool.mu.Unlock()
	return pool.stats
}
