/*
Copyright 2020 ceph-csi authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
	"os"
	"testing"
	"time"

	"github.com/ceph/go-ceph/rados"
)

const (
	interval = 15 * time.Minute
	expiry   = 10 * time.Minute
)

// fakeGet is used as a replacement for ConnPool.Get and does not need a
// working Ceph cluster to connect to.
//
// This is mostly a copy of ConnPool.Get().
func (cp *ConnPool) fakeGet(monitors, user, keyfile string) (*rados.Conn, string, error) {
	unique, err := cp.generateUniqueKey(monitors, user, keyfile)
	if err != nil {
		return nil, "", err
	}

	// need a lock while calling ce.touch()
	cp.lock.RLock()
	conn := cp.getConn(unique)
	cp.lock.RUnlock()
	if conn != nil {
		return conn, unique, nil
	}

	// cp.Get() creates and connects a rados.Conn here
	conn, err = rados.NewConn()
	if err != nil {
		return nil, "", err
	}

	ce := &connEntry{
		conn:     conn,
		lastUsed: time.Now(),
		users:    1,
	}

	cp.lock.Lock()
	defer cp.lock.Unlock()
	if oldConn := cp.getConn(unique); oldConn != nil {
		// there was a race, oldConn already exists
		ce.destroy()

		return oldConn, unique, nil
	}
	// this really is a new connection, add it to the map
	cp.conns[unique] = ce

	return conn, unique, nil
}

// nolint:paralleltest // these tests cannot run in parallel
func TestConnPool(t *testing.T) {
	cp := NewConnPool(interval, expiry)
	defer cp.Destroy()

	// create a keyfile with some contents
	keyfile := "/tmp/conn_utils.keyfile"
	err := os.WriteFile(keyfile, []byte("the-key"), 0o600)
	if err != nil {
		t.Errorf("failed to create keyfile: %v", err)

		return
	}
	defer os.Remove(keyfile)

	var conn *rados.Conn
	var unique string

	t.Run("fakeGet", func(t *testing.T) {
		conn, unique, err = cp.fakeGet("monitors", "user", keyfile)
		if err != nil {
			t.Errorf("failed to get connection: %v", err)
		}
		// prevent goanalysis_metalinter from complaining about unused conn
		_ = conn

		// there should be a single item in cp.conns
		if len(cp.conns) != 1 {
			t.Errorf("there is more than a single conn in cp.conns: %v", len(cp.conns))
		}

		// the ce should have a single user
		ce, exists := cp.conns[unique]
		if !exists {
			t.Errorf("getting the conn from cp.conns failed")
		}
		if ce.users != 1 {
			t.Errorf("there should only be one user: %v", ce.users)
		}
	})

	t.Run("doubleFakeGet", func(t *testing.T) {
		// after a 2nd get, there should still be a single conn in cp.conns
		_, _, err = cp.fakeGet("monitors", "user", keyfile)
		if err != nil {
			t.Errorf("failed to get connection: %v", err)
		}
		if len(cp.conns) != 1 {
			t.Errorf("a second conn was added to cp.conns: %v", len(cp.conns))
		}

		// the ce should have a two users
		ce, exists := cp.conns[unique]
		if !exists {
			t.Errorf("getting the conn from cp.conns failed")
		}
		if ce.users != 2 {
			t.Errorf("there should be two users: %v", ce.users)
		}

		cp.Put(ce.conn)
		if len(cp.conns) != 1 {
			t.Errorf("a single put should not remove all cp.conns: %v", len(cp.conns))
		}
		// the ce should have a single user again
		ce, exists = cp.conns[unique]
		if !exists {
			t.Errorf("getting the conn from cp.conns failed")
		}
		if ce.users != 1 {
			t.Errorf("There should only be one user: %v", ce.users)
		}
	})

	// there is still one conn in cp.conns after "doubleFakeGet"
	t.Run("garbageCollection", func(t *testing.T) {
		// timeout has not occurred yet, so number of conns in the list should stay the same
		cp.gc()
		if len(cp.conns) != 1 {
			t.Errorf("gc() should not have removed any entry from cp.conns: %v", len(cp.conns))
		}

		// force expiring the ConnEntry by fetching it and adjusting .lastUsed
		ce, exists := cp.conns[unique]
		if !exists {
			t.Error("getting the conn from cp.conns failed")
		}
		ce.lastUsed = ce.lastUsed.Add(-2 * expiry)

		if ce.users != 1 {
			t.Errorf("There should only be one user: %v", ce.users)
		}
		cp.Put(ce.conn)
		if ce.users != 0 {
			t.Errorf("There should be no users anymore: %v", ce.users)
		}

		// timeout has occurred now, so number of conns in the list should be less
		cp.gc()
		if len(cp.conns) != 0 {
			t.Errorf("gc() should have removed an entry from cp.conns: %v", len(cp.conns))
		}
	})
}
