package textstyles

import (
	"strings"
	"testing"

	"github.com/walles/moor/v2/twin"
	"gotest.tools/v3/assert"
)

func TestNextCharLastChar_base(t *testing.T) {
	s := styledStringSplitter{
		input: "a",
	}

	assert.Equal(t, 'a', s.nextChar())
	assert.Equal(t, 'a', s.lastChar())
	assert.Equal(t, rune(-1), s.nextChar())
	assert.Equal(t, rune(-1), s.lastChar())
}

func TestNextCharLastChar_empty(t *testing.T) {
	s := styledStringSplitter{
		input: "",
	}

	assert.Equal(t, rune(-1), s.nextChar())
	assert.Equal(t, rune(-1), s.lastChar())
}

func collectStyledStrings(s string) ([]_StyledString, twin.Style) {
	styledStrings := []_StyledString{}
	trailer := styledStringsFromString(twin.StyleDefault, s, nil, 0, func(str string, style twin.Style) int {
		styledStrings = append(styledStrings, _StyledString{str, style})

		// We render no cells, and we asked for no limit
		return 0
	})
	return styledStrings, trailer
}

// We should ignore OSC 133 sequences.
//
// Ref:
// https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
func TestIgnorePromptHints(t *testing.T) {
	// From an e-mail I got titled "moor question: "--RAW-CONTROL-CHARS" equivalent"
	styledStrings, trailer := collectStyledStrings("hell\x1b]133;A\x1b\\o")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "hello", styledStrings[0].String)
	assert.Equal(t, twin.StyleDefault, styledStrings[0].Style)

	// C rather than A, different end-of-sequence, should also be ignored
	styledStrings, trailer = collectStyledStrings("ass\x1b]133;C\x07ertion")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "assertion", styledStrings[0].String)
	assert.Equal(t, twin.StyleDefault, styledStrings[0].Style)
}

// We should ignore OSC queries. They are any sequence ending with a question
// mark, and expect some response from the terminal. We are not a terminal, so we
// ignore them.
//
// Ref: https://github.com/walles/moor/issues/279
func TestIgnoreQueries(t *testing.T) {
	styledStrings, trailer := collectStyledStrings("\x1b]11;?\x1b\\hello")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "hello", styledStrings[0].String)
	assert.Equal(t, twin.StyleDefault, styledStrings[0].Style)
}

// Unsure why colon separated colors exist, but the fact is that a number of
// things emit colon separated SGR codes. And numerous terminals accept them
// (search page for "delimiter"): https://github.com/kovidgoyal/kitty/issues/7
//
// Johan got an e-mail titled "moor question: "--RAW-CONTROL-CHARS" equivalent"
// about the sequence we're testing here.
func TestColonColors(t *testing.T) {
	styledStrings, trailer := collectStyledStrings("\x1b[38:5:238mhello")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "hello", styledStrings[0].String)
	assert.Equal(t, twin.StyleDefault.WithForeground(twin.NewColor256(238)), styledStrings[0].Style)
}

// Test handling of URLs with backslashes in them. Generated by some software on
// Windows, also accepted by less.
//
// Ref: https://github.com/walles/moor/issues/244
func TestWindowsURL(t *testing.T) {
	windowsPath := `src\detection\cpu\cpu_bsd.c`
	windowsCwd := `C:\msys64\home\zhang\fastfetch\src\detection\cpu\cpu_bsd.c`
	styledStrings, trailer := collectStyledStrings("\x1b]8;;vscode://file/" + windowsCwd + "\\" + windowsPath + "\x07" + windowsPath)
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, windowsPath, styledStrings[0].String)
}

func TestURLWithSpace(t *testing.T) {
	urlPath := `hello%20there.txt`
	text := `hello there.txt`
	styledStrings, trailer := collectStyledStrings("\x1b]8;;file://" + urlPath + "\x1b\\" + text)
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, text, styledStrings[0].String)
	assert.Assert(t, strings.Contains(styledStrings[0].Style.String(), "file://"+urlPath))
}

// Test handling of URLs with multiple parameters in OSC 8 sequences.
// Parameters are separated by colons in the format: id=value:key=value
//
// Ref: https://github.com/walles/moor/issues/403
func TestURLWithMultipleParameters(t *testing.T) {
	url := "http://example.com"
	text := "Link with parameters"
	styledStrings, trailer := collectStyledStrings("\x1b]8;id=link456:custom=value;" + url + "\x1b\\" + text + "\x1b]8;;\x1b\\")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, text, styledStrings[0].String)
	assert.Assert(t, strings.Contains(styledStrings[0].Style.String(), url))
}

// Test handling of URLs with empty id parameter in OSC 8 sequences.
// Empty parameters should not cause issues.
//
// Ref: https://github.com/walles/moor/issues/403
func TestURLWithEmptyID(t *testing.T) {
	url := "http://example.com"
	text := "Link with empty id"
	styledStrings, trailer := collectStyledStrings("\x1b]8;id=;" + url + "\x1b\\" + text + "\x1b]8;;\x1b\\")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, text, styledStrings[0].String)
	assert.Assert(t, strings.Contains(styledStrings[0].Style.String(), url))
}

func TestPlainTextColor(t *testing.T) {
	plainTextStyle := twin.StyleDefault.WithAttr(twin.AttrReverse)
	styledStrings := []_StyledString{}
	trailer := styledStringsFromString(plainTextStyle, "a\x1b[33mb\x1b[mc", nil, 0, func(str string, style twin.Style) int {
		styledStrings = append(styledStrings, _StyledString{str, style})

		// We render no cells, and we asked for no limit
		return 0
	})

	assert.Equal(t, 3, len(styledStrings))

	assert.Equal(t, "a", styledStrings[0].String)
	assert.Equal(t, plainTextStyle, styledStrings[0].Style)

	assert.Equal(t, "b", styledStrings[1].String)
	assert.Equal(t, plainTextStyle.WithForeground(twin.NewColor16(3)), styledStrings[1].Style)

	assert.Equal(t, "c", styledStrings[2].String)
	assert.Equal(t, plainTextStyle, styledStrings[2].Style)

	assert.Equal(t, plainTextStyle, trailer)
}

// A cells limit should bound how much of a styled run we buffer before asking
// the callback whether it has rendered enough. Without this, a leading color
// escape makes us buffer the entire line before the limit can stop parsing.
func TestCellsLimitFlushesLongStyledRun(t *testing.T) {
	const requestedCellsCount = 80
	line := "\x1b[31m" + strings.Repeat("x", 5*1024*1024)

	callbackCalls := 0
	styledStringsFromString(twin.StyleDefault, line, nil, requestedCellsCount, func(str string, _ twin.Style) int {
		callbackCalls++
		assert.Equal(t, len(str), requestedCellsCount*maxBytesPerCell)

		// All input runes in str are single-cell x characters, so the callback
		// has rendered the requested number of cells.
		return requestedCellsCount
	})

	assert.Equal(t, callbackCalls, 1)
}

// An escape sequence we can't make sense of gets replayed as plain runes. The
// cells limit should stop that replay as soon as the callback is full, just like
// it stops ordinary parsing.
func TestCellsLimitStopsMalformedEscapeReplay(t *testing.T) {
	const requestedCellsCount = 80

	// "ESC[" followed by parameter bytes and then "z", which is not a CSI type we
	// handle. Rendering it means replaying all those parameter bytes.
	line := "\x1b[" + strings.Repeat("3", 5*1024*1024) + "z"

	callbackCalls := 0
	styledStringsFromString(twin.StyleDefault, line, nil, requestedCellsCount, func(str string, _ twin.Style) int {
		callbackCalls++

		// The replay starts over from the ESC, so anything we get should come
		// from the start of the line
		assert.Assert(t, strings.HasPrefix(line, str),
			"got %d bytes from somewhere other than the start of the line", len(str))

		// Report the limit as reached, so there is no reason to keep going
		return requestedCellsCount
	})

	assert.Equal(t, callbackCalls, 1)
}

// Ignore G0 charset resets (`ESC(B`). They are output by "tput sgr0" on at
// least TERM=xterm-256color.
//
// Ref:
//   - https://github.com/walles/moor/issues/276
//   - https://www.xfree86.org/4.8.0/ctlseqs.html (look for "Designate G0" and
//     "USASCII")
func TestRestoreG0CharSet(t *testing.T) {
	styledStrings, trailer := collectStyledStrings("\x1b(Bhello")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "hello", styledStrings[0].String)
	assert.Equal(t, twin.StyleDefault, styledStrings[0].Style)
}

func TestUnsupportedG0CharSet(t *testing.T) {
	styledStrings, trailer := collectStyledStrings("\x1b(Xhello")
	assert.Equal(t, twin.StyleDefault, trailer)
	assert.Equal(t, 1, len(styledStrings))
	assert.Equal(t, "\x1b(Xhello", styledStrings[0].String)
}
