package databasefake_test

import (
	"context"
	"database/sql"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/coder/coder/coderd/database"

	"github.com/coder/coder/coderd/database/databasefake"
)

// test that transactions don't deadlock, and that we don't see intermediate state.
func TestInTx(t *testing.T) {
	t.Parallel()

	uut := databasefake.New()

	inTx := make(chan any)
	queriesDone := make(chan any)
	queriesStarted := make(chan any)
	go func() {
		err := uut.InTx(func(tx database.Store) error {
			close(inTx)
			_, err := tx.InsertOrganization(context.Background(), database.InsertOrganizationParams{
				Name: "1",
			})
			assert.NoError(t, err)
			<-queriesStarted
			time.Sleep(5 * time.Millisecond)
			_, err = tx.InsertOrganization(context.Background(), database.InsertOrganizationParams{
				Name: "2",
			})
			assert.NoError(t, err)
			return nil
		})
		assert.NoError(t, err)
	}()
	var nums []int
	go func() {
		<-inTx
		for i := 0; i < 20; i++ {
			orgs, err := uut.GetOrganizations(context.Background())
			if err != nil {
				assert.ErrorIs(t, err, sql.ErrNoRows)
			}
			nums = append(nums, len(orgs))
			time.Sleep(time.Millisecond)
		}
		close(queriesDone)
	}()
	close(queriesStarted)
	<-queriesDone
	// ensure we never saw 1 org, only 0 or 2.
	for i := 0; i < 20; i++ {
		assert.NotEqual(t, 1, nums[i])
	}
}

// TestExactMethods will ensure the fake database does not hold onto excessive
// functions. The fake database is a manual implementation, so it is possible
// we forget to delete functions that we remove. This unit test just ensures
// we remove the extra methods.
func TestExactMethods(t *testing.T) {
	t.Parallel()

	// extraFakeMethods contains the extra allowed methods that are not a part
	// of the database.Store interface.
	extraFakeMethods := map[string]string{
		// Example
		// "SortFakeLists": "Helper function used",
	}

	fake := reflect.TypeOf(databasefake.New())
	fakeMethods := methods(fake)

	store := reflect.TypeOf((*database.Store)(nil)).Elem()
	storeMethods := methods(store)

	// Store should be a subset
	for k := range storeMethods {
		_, ok := fakeMethods[k]
		if !ok {
			panic(fmt.Sprintf("This should never happen. FakeDB missing method %s, so doesn't fit the interface", k))
		}
		delete(storeMethods, k)
		delete(fakeMethods, k)
	}

	for k := range fakeMethods {
		_, ok := extraFakeMethods[k]
		if ok {
			continue
		}
		// If you are seeing this error, you have an extra function not required
		// for the database.Store. If you still want to keep it, add it to
		// 'extraFakeMethods' to allow it.
		t.Errorf("Fake method '%s()' is excessive and not needed to fit interface, delete it", k)
	}
}

func methods(rt reflect.Type) map[string]bool {
	methods := make(map[string]bool)
	for i := 0; i < rt.NumMethod(); i++ {
		methods[rt.Method(i).Name] = true
	}
	return methods
}
