package ulid

import (
	"sort"
	"strings"
	"testing"
	"time"
)

func TestNewShape(t *testing.T) {
	id := New()
	if len(id) != Len {
		t.Fatalf("len = %d, want %d", len(id), Len)
	}
	for i := 0; i < len(id); i++ {
		if !strings.ContainsRune(alphabet, rune(id[i])) {
			t.Fatalf("char %q at %d not in Crockford alphabet", id[i], i)
		}
	}
	if !IsValid(id) {
		t.Fatalf("IsValid(%q) = false", id)
	}
}

func TestUniqueness(t *testing.T) {
	const n = 10000
	seen := make(map[string]struct{}, n)
	for i := 0; i < n; i++ {
		id := New()
		if _, dup := seen[id]; dup {
			t.Fatalf("duplicate ULID %q after %d ids", id, i)
		}
		seen[id] = struct{}{}
	}
}

func TestLexicographicTimeOrdering(t *testing.T) {
	base := time.Now()
	var ids []string
	for i := 0; i < 50; i++ {
		ids = append(ids, At(base.Add(time.Duration(i)*time.Millisecond)))
	}
	if !sort.StringsAreSorted(ids) {
		t.Fatal("ULIDs with increasing timestamps are not lexicographically sorted")
	}
}

func TestTimestampRoundTrip(t *testing.T) {
	tests := []time.Time{
		time.UnixMilli(0),
		time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC),
		time.Now(),
	}
	for _, want := range tests {
		id := At(want)
		got, err := Timestamp(id)
		if err != nil {
			t.Fatalf("Timestamp(%q): %v", id, err)
		}
		if got.UnixMilli() != want.UnixMilli() {
			t.Errorf("timestamp = %v, want %v", got.UnixMilli(), want.UnixMilli())
		}
	}
}

func TestIsValid(t *testing.T) {
	tests := []struct {
		in   string
		want bool
	}{
		{New(), true},
		{strings.ToLower(New()), true}, // Crockford decoding is case-insensitive
		{"", false},
		{"short", false},
		{strings.Repeat("0", 25), false},
		{strings.Repeat("0", 27), false},
		{strings.Repeat("U", 26), false},       // U not in alphabet
		{"8" + strings.Repeat("0", 25), false}, // timestamp overflow (>48 bits)
		{"7" + strings.Repeat("Z", 25), true},
	}
	for _, tt := range tests {
		if got := IsValid(tt.in); got != tt.want {
			t.Errorf("IsValid(%q) = %v, want %v", tt.in, got, tt.want)
		}
	}
}

func TestTimestampRejectsInvalid(t *testing.T) {
	if _, err := Timestamp("definitely-not-a-ulid!!!!!"); err == nil {
		t.Fatal("want error")
	}
}
