// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package lru_test

import (
	"bytes"
	cryptorand "crypto/rand"
	"fmt"
	"log"
	mathrand "math/rand"
	"strings"
	"testing"

	"golang.org/x/sync/errgroup"
	"golang.org/x/tools/gopls/internal/util/lru"
)

func TestCache(t *testing.T) {
	type get struct {
		key  string
		want any
	}
	type set struct {
		key, value string
	}

	tests := []struct {
		label string
		steps []any
	}{
		{"empty cache", []any{
			get{"a", nil},
			get{"b", nil},
		}},
		{"zero-length string", []any{
			set{"a", ""},
			get{"a", ""},
		}},
		{"under capacity", []any{
			set{"a", "123"},
			set{"b", "456"},
			get{"a", "123"},
			get{"b", "456"},
		}},
		{"over capacity", []any{
			set{"a", "123"},
			set{"b", "456"},
			set{"c", "78901"},
			get{"a", nil},
			get{"b", "456"},
			get{"c", "78901"},
		}},
		{"access ordering", []any{
			set{"a", "123"},
			set{"b", "456"},
			get{"a", "123"},
			set{"c", "78901"},
			get{"a", "123"},
			get{"b", nil},
			get{"c", "78901"},
		}},
	}

	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			c := lru.New(10)
			for i, step := range test.steps {
				switch step := step.(type) {
				case get:
					if got := c.Get(step.key); got != step.want {
						t.Errorf("#%d: c.Get(%q) = %q, want %q", i, step.key, got, step.want)
					}
				case set:
					c.Set(step.key, step.value, len(step.value))
				}
			}
		})
	}
}

// TestConcurrency exercises concurrent access to the same entry.
//
// It is a copy of TestConcurrency from the filecache package.
func TestConcurrency(t *testing.T) {
	key := uniqueKey()
	const N = 100 // concurrency level

	// Construct N distinct values, each larger
	// than a typical 4KB OS file buffer page.
	var values [N][8192]byte
	for i := range values {
		if _, err := mathrand.Read(values[i][:]); err != nil {
			t.Fatalf("rand: %v", err)
		}
	}

	cache := lru.New(100 * 1e6) // 100MB cache

	// get calls Get and verifies that the cache entry
	// matches one of the values passed to Set.
	get := func(mustBeFound bool) error {
		got := cache.Get(key)
		if got == nil {
			if !mustBeFound {
				return nil
			}
			return fmt.Errorf("Get did not return a value")
		}
		gotBytes := got.([]byte)
		for _, want := range values {
			if bytes.Equal(want[:], gotBytes) {
				return nil // a match
			}
		}
		return fmt.Errorf("Get returned a value that was never Set")
	}

	// Perform N concurrent calls to Set and Get.
	// All sets must succeed.
	// All gets must return nothing, or one of the Set values;
	// there is no third possibility.
	var group errgroup.Group
	for i := range values {
		i := i
		v := values[i][:]
		group.Go(func() error {
			cache.Set(key, v, len(v))
			return nil
		})
		group.Go(func() error { return get(false) })
	}
	if err := group.Wait(); err != nil {
		if strings.Contains(err.Error(), "operation not supported") ||
			strings.Contains(err.Error(), "not implemented") {
			t.Skipf("skipping: %v", err)
		}
		t.Fatal(err)
	}

	// A final Get must report one of the values that was Set.
	if err := get(true); err != nil {
		t.Fatalf("final Get failed: %v", err)
	}
}

// uniqueKey returns a key that has never been used before.
func uniqueKey() (key [32]byte) {
	if _, err := cryptorand.Read(key[:]); err != nil {
		log.Fatalf("rand: %v", err)
	}
	return
}
