package service

import (
	"regexp"
	"strings"
	"testing"
)

// fuzzSpellingRe mirrors the token charset mentionTokenRe captures: letters,
// digits, underscore, dot and hyphen, 1-64 runes. Every candidate spelling
// parseMentionTokens returns must match it -- if it doesn't, the parser
// leaked something outside its own documented token shape.
var fuzzSpellingRe = regexp.MustCompile(`^[\p{L}\p{N}_.-]{1,64}$`)

// FuzzParseMentionTokens shakes out panics and contract violations in
// parseMentionTokens: unicode, RTL overrides, combining marks, null bytes,
// pathological runs of "@", and address-shaped text that must never yield a
// bare mention.
func FuzzParseMentionTokens(f *testing.F) {
	seeds := []string{
		"",
		"@bob",
		"@@bob",
		"@@@",
		"mail@example.com",
		"a@b",
		"@a@b@c",
		"@bob@example.com",
		"@everyone",
		"@here",
		"@EVERYONE",
		"@Here",
		strings.Repeat("@", 10000),
		strings.Repeat("@a", 5000),
		"@bob.",
		"@bob-",
		"@bob_baz.qux",
		"\U0001F642@bob\U0001F642", // emoji either side of the token
		" @bob ",
		"cafe\u0301@bob",   // combining acute accent (e + U+0301)
		"\u202e@bob\u202e", // RTL override (U+202E)
		"\u200f@bob",       // RTL mark (U+200F)
		"@\u0301",          // bare combining mark as the token itself
		"@bob\x00carol",    // embedded NUL
		strings.Repeat("a", 2000) + "@" + strings.Repeat("b", 2000),
		"@" + strings.Repeat("x", 100),
		"@" + strings.Repeat("x", 63) + "y" + strings.Repeat("z", 63),
		"(@bob), @carol!",
		"@" + strings.Repeat("\U0001F600", 100),
	}
	for _, s := range seeds {
		f.Add(s)
	}

	f.Fuzz(func(t *testing.T, content string) {
		tokens, everyone, here := parseMentionTokens(content)

		if len(tokens) > maxMentionCandidates {
			t.Fatalf("token count %d exceeds cap %d", len(tokens), maxMentionCandidates)
		}

		seen := make(map[string]bool, len(tokens))
		for _, tok := range tokens {
			if len(tok.spellings) == 0 {
				t.Fatalf("candidate with zero spellings for content %q", content)
			}
			for _, sp := range tok.spellings {
				if sp == "" {
					t.Fatalf("empty spelling in candidate for content %q", content)
				}
				if strings.Contains(sp, "@") {
					t.Fatalf("spelling %q retained an '@' -- address-shaped text leaked a mention (content %q)", sp, content)
				}
				if sp != strings.ToLower(sp) {
					t.Fatalf("spelling %q is not lowercased", sp)
				}
				if !fuzzSpellingRe.MatchString(sp) {
					t.Fatalf("spelling %q outside the token charset (content %q)", sp, content)
				}
				if sp == everyoneToken || sp == hereToken {
					t.Fatalf("reserved token %q leaked into resolvable candidates", sp)
				}
			}
			primary := tok.spellings[0]
			if seen[primary] {
				t.Fatalf("duplicate candidate %q returned (content %q)", primary, content)
			}
			seen[primary] = true
		}

		// @everyone/@here flags must only ever be set for the literal reserved
		// words; the substring check is a necessary (not sufficient) condition
		// that still catches a parser gone wrong on unrelated input.
		lower := strings.ToLower(content)
		if everyone && !strings.Contains(lower, everyoneToken) {
			t.Fatalf("everyone=true but content has no %q substring: %q", everyoneToken, content)
		}
		if here && !strings.Contains(lower, hereToken) {
			t.Fatalf("here=true but content has no %q substring: %q", hereToken, content)
		}
	})
}
