package ident

import (
	"strings"
	"sync"
	"testing"

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

// Идентификатор — ULID в нижнем регистре: постоянная ширина, алфавит Crockford,
// и разбор своего же значения его не меняет.
func TestNewIsCanonical(t *testing.T) {
	id := New()

	assert.Len(t, id, Len)
	assert.Equal(t, strings.ToLower(id), id, "идентификатор выдан не в нижнем регистре")

	parsed, ok := Parse(id)
	require.True(t, ok, "свой же идентификатор не разобрался")
	assert.Equal(t, id, parsed, "разбор изменил канонический вид")
}

// Порядок выдачи и порядок сравнения строк совпадают — в том числе внутри одной
// миллисекунды: колонка времени несёт секунды, и «новые сверху» держит ключ.
func TestNewIsMonotonic(t *testing.T) {
	previous := New()
	for range 10000 {
		next := New()
		require.Less(t, previous, next, "идентификатор выдан не по возрастанию")
		previous = next
	}
}

// Выдача идёт из нескольких потоков разом: два одинаковых ключа означали бы
// отказ вставки на живой базе.
func TestNewIsUniqueUnderConcurrency(t *testing.T) {
	const goroutines = 8
	const each = 500

	var (
		mu  sync.Mutex
		all = map[string]bool{}
	)

	var wg sync.WaitGroup
	for range goroutines {
		wg.Add(1)
		go func() {
			defer wg.Done()
			own := make([]string, 0, each)
			for range each {
				own = append(own, New())
			}
			mu.Lock()
			defer mu.Unlock()
			for _, id := range own {
				all[id] = true
			}
		}()
	}
	wg.Wait()

	assert.Len(t, all, goroutines*each, "выданы одинаковые идентификаторы")
}

// Разбор проверяет вид и приводит регистр: значение приходит от спрашивающего, а
// сравнение в базе побайтово.
func TestParse(t *testing.T) {
	id := New()

	upper, ok := Parse(strings.ToUpper(id))
	require.True(t, ok, "верхний регистр не принят: алфавит к нему нечувствителен")
	assert.Equal(t, id, upper, "разбор не привёл регистр к каноническому")

	for name, value := range map[string]string{
		"пусто":             "",
		"короче":            id[:Len-1],
		"длиннее":           id + "0",
		"знак вне алфавита": strings.Repeat("u", Len),
		"время за пределом": "8" + id[1:],
		"не латиница":       strings.Repeat("я", Len),
	} {
		t.Run(name, func(t *testing.T) {
			_, ok := Parse(value)
			assert.False(t, ok, "негодное значение принято разбором")
		})
	}
}

// Время, зашитое в идентификатор, идёт в ту же сторону, что и колонка времени.
func TestTimestampGrowsWithIssue(t *testing.T) {
	first, ok := Timestamp(New())
	require.True(t, ok)

	second, ok := Timestamp(New())
	require.True(t, ok)

	assert.False(t, second.Before(first), "время в ключе пошло назад")
}
