package sessioncache

import (
	"context"
	"database/sql"
	sqldriver "database/sql/driver"
	"io"
	"sync"
	"testing"

	comerrs "dataservices/commons/errs"

	"github.com/stretchr/testify/require"
)

type testCloseTracker struct {
	mu     sync.Mutex
	closed int
}

type testDriver struct {
	tracker *testCloseTracker
}

type testConn struct {
	tracker *testCloseTracker
}

type testConnector struct {
	tracker *testCloseTracker
}

func (d *testDriver) Open(name string) (sqldriver.Conn, error) {
	return &testConn{tracker: d.tracker}, nil
}

func (c *testConn) Prepare(query string) (sqldriver.Stmt, error) { return nil, io.EOF }
func (c *testConn) Begin() (sqldriver.Tx, error)                 { return nil, io.EOF }
func (c *testConn) Close() error {
	c.tracker.mu.Lock()
	defer c.tracker.mu.Unlock()
	c.tracker.closed++
	return nil
}

func (c *testConnector) Connect(context.Context) (sqldriver.Conn, error) {
	return &testConn{tracker: c.tracker}, nil
}

func (c *testConnector) Driver() sqldriver.Driver {
	return &testDriver{tracker: c.tracker}
}

func openTrackedDB(t *testing.T, tracker *testCloseTracker) *sql.DB {
	t.Helper()
	db := sql.OpenDB(&testConnector{tracker: tracker})
	conn, err := db.Conn(context.Background())
	require.NoError(t, err)
	require.NoError(t, conn.Close())
	return db
}

func TestRegisterLocalDriver(t *testing.T) {
	oldImpl := sessionImpl
	oldStorage := storage
	defer func() {
		sessionImpl = oldImpl
		storage = oldStorage
	}()

	storage = &SessionCache{}
	RegisterLocalDriver(context.Background())
	require.IsType(t, &localSession{}, driver())

	item := &SessionItem{
		SessionID: "s1",
		Database:  "test",
		ClusterID: 42,
		ExpireSec: 60,
	}
	require.NoError(t, driver().Save(context.Background(), item))

	got, err := driver().Get(context.Background(), "s1")
	require.NoError(t, err)
	require.Equal(t, item, got)

	loaded := &SessionItem{SessionID: "s1"}
	require.NoError(t, driver().Load(context.Background(), loaded))
	require.Equal(t, *item, *loaded)

	require.NoError(t, driver().Delete(context.Background(), "s1"))
	_, err = driver().Get(context.Background(), "s1")
	require.ErrorIs(t, err, comerrs.ErrInvalidSession)
}

func TestLocalSessionDeleteClosesDB(t *testing.T) {
	oldImpl := sessionImpl
	oldStorage := storage
	defer func() {
		sessionImpl = oldImpl
		storage = oldStorage
	}()

	storage = &SessionCache{}
	RegisterLocalDriver(context.Background())

	tracker := &testCloseTracker{}
	db := openTrackedDB(t, tracker)
	item := CreateSession("s1", db, "test", "crt", 42, 60, nil, nil)

	require.NoError(t, driver().Delete(context.Background(), item.SessionID))
	_, err := driver().Get(context.Background(), item.SessionID)
	require.ErrorIs(t, err, comerrs.ErrInvalidSession)
	require.Equal(t, 1, tracker.closed)
}

func TestResetConnClosesOldDB(t *testing.T) {
	oldStorage := storage
	defer func() {
		storage = oldStorage
	}()

	storage = &SessionCache{}

	oldTracker := &testCloseTracker{}
	newTracker := &testCloseTracker{}
	oldDB := openTrackedDB(t, oldTracker)
	newDB := openTrackedDB(t, newTracker)
	item := CreateSession("s1", oldDB, "test", "crt", 42, 60, nil, nil)

	require.NoError(t, ResetConn(item.SessionID, newDB))
	require.Equal(t, 1, oldTracker.closed)
	require.Equal(t, 0, newTracker.closed)
}

func TestClearExpiredSessionsClosesDB(t *testing.T) {
	oldStorage := storage
	defer func() {
		storage = oldStorage
	}()

	storage = &SessionCache{}

	tracker := &testCloseTracker{}
	db := openTrackedDB(t, tracker)
	item := CreateSession("s1", db, "test", "crt", 42, 1, nil, nil)
	item.EndSec = 1

	num := clearExpiredSessions(storage, 2)
	require.Equal(t, 1, num)
	_, err := loadSession(item.SessionID)
	require.Error(t, err)
	require.Equal(t, 1, tracker.closed)
}
