package lexer

import (
	"strings"
	"testing"
)

// The highlighter emits HTML, and its input is Go source — which is full of <, > and &.
// Anything that reaches the page unescaped is markup injection into your own docs page:
// a snippet containing `<div>` would render a div.
func TestHighlightGoEscapes(t *testing.T) {
	got := HighlightGo(`s := "<div class=\"x\">" // a & b`)

	if strings.Contains(got, "<div") {
		t.Errorf("a `<div>` in the SOURCE reached the output as markup:\n%s", got)
	}
	if !strings.Contains(got, "&lt;div") {
		t.Errorf("the angle bracket was not escaped:\n%s", got)
	}
	if !strings.Contains(got, "&amp;") {
		t.Errorf("the ampersand was not escaped:\n%s", got)
	}
}

func TestHighlightGoClassifies(t *testing.T) {
	got := HighlightGo("func main() { x := 42 // note\n}")

	for _, want := range []struct{ what, class, text string }{
		{"keyword", KeywordClass, "func"},
		{"call", FuncClass, "main"},
		{"number", NumberClass, "42"},
		{"comment", CommentClass, "// note"},
	} {
		if !strings.Contains(got, `<span class="`+want.class+`">`+want.text+`</span>`) {
			t.Errorf("%s %q was not highlighted:\n%s", want.what, want.text, got)
		}
	}
}

// The //gowasm: directives are the most important line in half these snippets. They are
// comments, and must survive as such.
func TestHighlightGoKeepsDirectives(t *testing.T) {
	got := HighlightGo("//gowasm:page / static layout=public\nfunc HomePage() {}")
	if !strings.Contains(got, `<span class="`+CommentClass+`">//gowasm:page / static layout=public</span>`) {
		t.Errorf("the directive was not kept whole as a comment:\n%s", got)
	}
}

// A lexer that can hang or eat the rest of the file on malformed input would take the
// whole page down with it. Unterminated literals stop; they do not run away.
func TestHighlightGoSurvivesMalformedInput(t *testing.T) {
	for _, src := range []string{
		`x := "unterminated`,
		"y := `unterminated raw",
		"/* unterminated block",
		`z := '`,
		"",
	} {
		got := HighlightGo(src)
		// The text must all still be there — mangling is not an acceptable failure mode
		// either. Compare on the visible characters, ignoring the spans.
		if plain := stripTags(got); plain != src {
			t.Errorf("input %q came out as %q", src, plain)
		}
	}
}

// Nothing is dropped: every byte of the source is still on the page, in order.
func TestHighlightGoIsLossless(t *testing.T) {
	src := "package app\n\nimport \"strings\"\n\nfunc f(n int) string {\n\treturn strings.Repeat(\"x\", n) // pad\n}\n"
	if plain := stripTags(HighlightGo(src)); plain != src {
		t.Errorf("the highlighter changed the source.\n got: %q\nwant: %q", plain, src)
	}
}

// stripTags removes the spans and unescapes, recovering the original source.
func stripTags(s string) string {
	var b strings.Builder
	for i := 0; i < len(s); {
		if s[i] == '<' {
			j := strings.IndexByte(s[i:], '>')
			if j < 0 {
				break
			}
			i += j + 1
			continue
		}
		b.WriteByte(s[i])
		i++
	}
	out := b.String()
	// Reverse html.EscapeString, innermost last.
	out = strings.ReplaceAll(out, "&lt;", "<")
	out = strings.ReplaceAll(out, "&gt;", ">")
	out = strings.ReplaceAll(out, "&#34;", `"`)
	out = strings.ReplaceAll(out, "&#39;", "'")
	out = strings.ReplaceAll(out, "&amp;", "&")
	return out
}
