package oneid

import (
	"log"
	"os"
	"sync"
	"testing"
)

// TestNewUint64ConfigMinValues tests NewUint64Config for using minimal values configuration.
func TestNewUint64ConfigMinValues(t *testing.T) {
	t.Parallel()

	c := NewUint64Config(0, 0, 0)

	if c.ProcessBits != minUint64ProcessBits {
		t.Error("ProcessBits is not set to minimum value")
	}

	if c.ServerBits != minUint64ServerBits {
		t.Error("ServerBits is not set to minimum value", c.ServerBits)
	}

	if c.SequenceBits < minUint64SequenceBits {
		t.Error("SequenceBits is not set to minimum value")
	}
}

// TestNewUint64ConfigSequanceBitsIsMax tests NewUint64Config SequenceBits is set to the maximum value.
func TestNewUint64ConfigSequanceBitsIsMax(t *testing.T) {
	t.Parallel()

	values := [3][4]uint64{
		{0, 0, 0, totalUint64Bits - 2},
		{
			defaultUint64ProcessBits, defaultUint64ServerBits,
			defaultUint64SequenceBits, totalUint64Bits - defaultUint64ProcessBits - defaultUint64ServerBits,
		},
		{
			minUint64ProcessBits, minUint64ServerBits,
			minUint64SequenceBits, totalUint64Bits - minUint64ProcessBits - minUint64ServerBits,
		},
	}

	for _, v := range values {
		c := NewUint64Config(v[0], v[1], v[2])

		if c.SequenceBits != v[3] {
			t.Error("SequenceBits is not maxed out for values", v[0], v[1], v[2], "Expected:", v[3])
		}
	}
}

// TestNewUint64ConfigTotalBitsLength tests NewUint64Config total bits length.
func estNewUint64ConfigTotalBitsLength(t *testing.T) {
	t.Parallel()

	values := [5][3]uint64{
		{0, 0, 0},
		{5, 10, 20},
		{10, 20, 30},
		{30, 50, 60},
		{108, 200, 300},
	}

	for _, v := range values {
		c := NewUint64Config(v[0], v[1], v[2])

		if c.ProcessBits+c.ServerBits+c.SequenceBits != totalUint64Bits {
			t.Error("Total bits is not equal to", totalUint64Bits)
		}
	}
}

// TestNewCustomUint6ZeroId tests CustomUint64 for any zero id.
func TestNewCustomUint64ZeroId(t *testing.T) {
	t.Parallel()

	// create config with default values
	c := NewUint64Config(defaultUint64ProcessBits, defaultUint64ServerBits, defaultUint64SequenceBits)

	for i := uint64(0); i < 10_000; i++ {
		if Uint64(i, 0, &c) == 0 {
			t.Error("Zero Id found with serverID:", i)
		}
	}
}

// TestNewCustomUint64NonDuplicateId tests CustomUint64 for any duplicate id.
func TestNewCustomUint64DuplicateId(t *testing.T) {
	t.Parallel()

	// create config with default values
	c := NewUint64Config(defaultUint64ProcessBits, defaultUint64ServerBits, defaultUint64SequenceBits)

	var ids []uint64

	for i := uint64(0); i < 10_000; i++ {
		id := Uint64(i, 0, &c)

		for _, v := range ids {
			if id == v {
				t.Error("Duplicate Id found: ", id)
			}
		}

		ids = append(ids, id)
	}
}

// TestNewCustomUint64ForDuplicateIdMultipleThreads tests CustomUint64 for any duplicate id.
func TestNewCustomUint64NonDuplicateIdMultipleThreads(t *testing.T) {
	t.Parallel()

	ids := make(chan uint64, 100_000_000)
	wg := &sync.WaitGroup{}
	wg.Add(10_000)

	for t := 0; t < 10_000; t++ {
		go func() {
			defer wg.Done()

			for i := uint64(0); i < 10_000; i++ {
				id := Uint64(i, 0, &DefaultUint64Config)

				ids <- id
			}
		}()
	}

	wg.Wait()
	close(ids)

	seen := make(map[uint64]struct{})

	for i := range ids {
		if _, ok := seen[i]; !ok {
			seen[i] = struct{}{}
		} else {
			t.Error("Duplicate Id found: ", i)
		}
	}
}

// TestUint64ZeroServerID calls Uint64() with server id = 0.
// checks for returning a non zero id
func TestUint64ZeroServerID(t *testing.T) {
	t.Parallel()

	if Uint64(1, 0, &DefaultUint64Config) == 0 {
		t.Error("ID equals zero")
	}
}

// TestUint64ForNonUniqueIdOnSameProcessAndServer tests Uint64() serially for any duplicate ids generated
// using same serverID.
func TestUint64ForNonUniqueIdsOnSameProcessAndServer(t *testing.T) {
	t.Parallel()

	var ids []uint64

	for c := 0; c < 100_000; c++ {
		id := Uint64(1, 0, &DefaultUint64Config)

		for _, v := range ids {
			if v == id {
				t.Error("Duplicate Id found id:", id)
			}
		}
		ids = append(ids, id)
	}
}

// TestUint64ForNonUniqueIdOnSameProcessAndServerAcrossMultipleThreads tests Uint64() concurrently for
// any duplicate id generated  using same serverID.
func TestUint64ForDuplicateIdOnSameProcessAndServerAcrossMultipleThreads(t *testing.T) {
	t.Parallel()

	ids := make(chan uint64, 100_000_000)
	wg := &sync.WaitGroup{}

	wg.Add(10_000)
	for p := 0; p < 10_000; p++ {
		go func() {
			defer wg.Done()

			for c := 0; c < 10_000; c++ {
				ids <- Uint64(1, 0, &DefaultUint64Config)
			}
		}()
	}

	wg.Wait()

	close(ids)

	seen := make(map[uint64]struct{})

	for i := range ids {
		if _, ok := seen[i]; !ok {
			seen[i] = struct{}{}
		} else {
			t.Error("Duplicate Id found:", i)
		}
	}
}

// TestUint64ForNonUniqueIdOnDifferentServerIDs tests Uint64() serially for any duplicate ids generated
// using different serverIDs upto the maximum 1024.
func TestUint64ForNonUniqueIdOnDifferentServerIDs(t *testing.T) {
	t.Parallel()

	var ids []uint64

	for c := uint64(1); c < 1025; c++ {
		id := Uint64(1, 0, &DefaultUint64Config)

		for _, v := range ids {
			if v == id {
				t.Error("Duplicate Id found with serverID:", c, "id:", id)
			}
		}

		ids = append(ids, id)
	}
}

// TestUint64ForNonUniqueIdOnDifferentServerIDsAcrossMultipleThreads tests Uint64()
// concurrently for any duplicate ids generated
// using different serverIDs upto the maximum 1024.
func TestUint64ForNonUniqueIdOnDifferentServerIDsAcrossMultipleThreads(t *testing.T) {
	t.Parallel()

	ids := make(chan uint64, 10_240_000)
	wg := &sync.WaitGroup{}

	wg.Add(10_000)

	for p := 0; p < 10_000; p++ {
		go func() {
			defer wg.Done()

			for c := uint64(0); c < 1024; c++ {
				ids <- Uint64(1, 0, &DefaultUint64Config)
			}
		}()
	}

	wg.Wait()
	close(ids)

	seen := make(map[uint64]struct{})

	for i := range ids {
		if _, ok := seen[i]; !ok {
			seen[i] = struct{}{}
		} else {
			t.Error("Duplicate Id found:", i)
		}
	}
}

// TestEnvUint64 calls EnvUint64 with custom env variables.
func TestEnvUint64(t *testing.T) {
	t.Parallel()

	cleanEnvVars()

	data := []EnvTestData{
		{
			ServerID:  "",
			ProcessID: "",
			IsError:   true,
		},
		{
			ServerID:  "0",
			ProcessID: "0",
			IsError:   false,
		},
		{
			ServerID:  "-1",
			ProcessID: "-1",
			IsError:   true,
		},
		{
			ServerID:  " ",
			ProcessID: " ",
			IsError:   true,
		},
		{
			ServerID:  "100_000",
			ProcessID: "100_000",
			IsError:   true,
		},
		{
			ServerID:  "1",
			ProcessID: "1",
			IsError:   false,
		},
		{
			ServerID:  "100",
			ProcessID: "100",
			IsError:   false,
		},
	}

	for _, v := range data {
		err := os.Setenv(serverIDKey, v.ServerID)
		if err != nil {
			log.Fatalln("failed to set env", serverIDKey, "to", v.ServerID)
		}

		err = os.Setenv(processIDKey, v.ProcessID)
		if err != nil {
			log.Fatalln("failed to set env", processIDKey, "to", v.ProcessID)
		}

		id, err := EnvUint64(&DefaultUint64Config)
		if err == nil && v.IsError {
			t.Error("expected error found none.",
				"ServerID:", v.ServerID,
				"ProcessID:", v.ProcessID,
				"generated ID:", id,
			)
		}

		if err != nil && !v.IsError {
			t.Error("expected no error, found one, error:", err)
		}
	}
}

// BenchmarkUint64 benchmarks a Uint64(1).
func BenchmarkUint64(b *testing.B) {
	for c := 0; c < b.N; c++ {
		_ = Uint64(1, 0, &DefaultUint64Config)
	}
}