// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package protocol_test

import (
	"fmt"
	"strings"
	"testing"

	"golang.org/x/tools/gopls/internal/protocol"
)

// This file tests Mapper's logic for converting between offsets,
// UTF-8 columns, and UTF-16 columns. (The strange form attests to
// earlier abstractions.)

// 𐐀 is U+10400 = [F0 90 90 80] in UTF-8, [D801 DC00] in UTF-16.
var funnyString = []byte("𐐀23\n𐐀45")

var toUTF16Tests = []struct {
	scenario    string
	input       []byte
	line        int    // 1-indexed count
	col         int    // 1-indexed byte position in line
	offset      int    // 0-indexed byte offset into input
	resUTF16col int    // 1-indexed UTF-16 col number
	pre         string // everything before the cursor on the line
	post        string // everything from the cursor onwards
	err         string // expected error string in call to ToUTF16Column
	issue       *bool
}{
	{
		scenario: "cursor missing content",
		input:    nil,
		offset:   -1,
		err:      "point has neither offset nor line/column",
	},
	{
		scenario: "cursor missing position",
		input:    funnyString,
		line:     -1,
		col:      -1,
		offset:   -1,
		err:      "point has neither offset nor line/column",
	},
	{
		scenario:    "zero length input; cursor at first col, first line",
		input:       []byte(""),
		line:        1,
		col:         1,
		offset:      0,
		resUTF16col: 1,
	},
	{
		scenario:    "cursor before funny character; first line",
		input:       funnyString,
		line:        1,
		col:         1,
		offset:      0,
		resUTF16col: 1,
		pre:         "",
		post:        "𐐀23",
	},
	{
		scenario:    "cursor after funny character; first line",
		input:       funnyString,
		line:        1,
		col:         5, // 4 + 1 (1-indexed)
		offset:      4, // (unused since we have line+col)
		resUTF16col: 3, // 2 + 1 (1-indexed)
		pre:         "𐐀",
		post:        "23",
	},
	{
		scenario:    "cursor after last character on first line",
		input:       funnyString,
		line:        1,
		col:         7, // 4 + 1 + 1 + 1 (1-indexed)
		offset:      6, // 4 + 1 + 1 (unused since we have line+col)
		resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed)
		pre:         "𐐀23",
		post:        "",
	},
	{
		scenario:    "cursor before funny character; second line",
		input:       funnyString,
		line:        2,
		col:         1,
		offset:      7, // length of first line (unused since we have line+col)
		resUTF16col: 1,
		pre:         "",
		post:        "𐐀45",
	},
	{
		scenario:    "cursor after funny character; second line",
		input:       funnyString,
		line:        1,
		col:         5,  // 4 + 1 (1-indexed)
		offset:      11, // 7 (length of first line) + 4 (unused since we have line+col)
		resUTF16col: 3,  // 2 + 1 (1-indexed)
		pre:         "𐐀",
		post:        "45",
	},
	{
		scenario:    "cursor after last character on second line",
		input:       funnyString,
		line:        2,
		col:         7,  // 4 + 1 + 1 + 1 (1-indexed)
		offset:      13, // 7 (length of first line) + 4 + 1 + 1 (unused since we have line+col)
		resUTF16col: 5,  // 2 + 1 + 1 + 1 (1-indexed)
		pre:         "𐐀45",
		post:        "",
	},
	{
		scenario: "cursor beyond end of file",
		input:    funnyString,
		line:     2,
		col:      8,  // 4 + 1 + 1 + 1 + 1 (1-indexed)
		offset:   14, // 4 + 1 + 1 + 1 (unused since we have line+col)
		err:      "column is beyond end of file",
	},
}

var fromUTF16Tests = []struct {
	scenario  string
	input     []byte
	line      int    // 1-indexed line number (isn't actually used)
	utf16col  int    // 1-indexed UTF-16 col number
	resCol    int    // 1-indexed byte position in line
	resOffset int    // 0-indexed byte offset into input
	pre       string // everything before the cursor on the line
	post      string // everything from the cursor onwards
	err       string // expected error string in call to ToUTF16Column
}{
	{
		scenario:  "zero length input; cursor at first col, first line",
		input:     []byte(""),
		line:      1,
		utf16col:  1,
		resCol:    1,
		resOffset: 0,
		pre:       "",
		post:      "",
	},
	{
		scenario:  "cursor before funny character",
		input:     funnyString,
		line:      1,
		utf16col:  1,
		resCol:    1,
		resOffset: 0,
		pre:       "",
		post:      "𐐀23",
	},
	{
		scenario:  "cursor after funny character",
		input:     funnyString,
		line:      1,
		utf16col:  3,
		resCol:    5,
		resOffset: 4,
		pre:       "𐐀",
		post:      "23",
	},
	{
		scenario:  "cursor after last character on line",
		input:     funnyString,
		line:      1,
		utf16col:  5,
		resCol:    7,
		resOffset: 6,
		pre:       "𐐀23",
		post:      "",
	},
	{
		scenario:  "cursor beyond last character on line",
		input:     funnyString,
		line:      1,
		utf16col:  6,
		resCol:    7,
		resOffset: 6,
		pre:       "𐐀23",
		post:      "",
		err:       "column is beyond end of line",
	},
	{
		scenario:  "cursor before funny character; second line",
		input:     funnyString,
		line:      2,
		utf16col:  1,
		resCol:    1,
		resOffset: 7,
		pre:       "",
		post:      "𐐀45",
	},
	{
		scenario:  "cursor after funny character; second line",
		input:     funnyString,
		line:      2,
		utf16col:  3,  // 2 + 1 (1-indexed)
		resCol:    5,  // 4 + 1 (1-indexed)
		resOffset: 11, // 7 (length of first line) + 4
		pre:       "𐐀",
		post:      "45",
	},
	{
		scenario:  "cursor after last character on second line",
		input:     funnyString,
		line:      2,
		utf16col:  5,  // 2 + 1 + 1 + 1 (1-indexed)
		resCol:    7,  // 4 + 1 + 1 + 1 (1-indexed)
		resOffset: 13, // 7 (length of first line) + 4 + 1 + 1
		pre:       "𐐀45",
		post:      "",
	},
	{
		scenario:  "cursor beyond end of file",
		input:     funnyString,
		line:      2,
		utf16col:  6,  // 2 + 1 + 1 + 1 + 1(1-indexed)
		resCol:    8,  // 4 + 1 + 1 + 1 + 1 (1-indexed)
		resOffset: 14, // 7 (length of first line) + 4 + 1 + 1 + 1
		err:       "column is beyond end of file",
	},
}

func TestToUTF16(t *testing.T) {
	for _, e := range toUTF16Tests {
		t.Run(e.scenario, func(t *testing.T) {
			if e.issue != nil && !*e.issue {
				t.Skip("expected to fail")
			}
			m := protocol.NewMapper("", e.input)
			var pos protocol.Position
			var err error
			if e.line > 0 {
				pos, err = m.LineCol8Position(e.line, e.col)
			} else if e.offset >= 0 {
				pos, err = m.OffsetPosition(e.offset)
			} else {
				err = fmt.Errorf("point has neither offset nor line/column")
			}
			if err != nil {
				if err.Error() != e.err {
					t.Fatalf("expected error %v; got %v", e.err, err)
				}
				return
			}
			if e.err != "" {
				t.Fatalf("unexpected success; wanted %v", e.err)
			}
			got := int(pos.Character) + 1
			if got != e.resUTF16col {
				t.Fatalf("expected result %v; got %v", e.resUTF16col, got)
			}
			pre, post := getPrePost(e.input, e.offset)
			if pre != e.pre {
				t.Fatalf("expected #%d pre %q; got %q", e.offset, e.pre, pre)
			}
			if post != e.post {
				t.Fatalf("expected #%d, post %q; got %q", e.offset, e.post, post)
			}
		})
	}
}

func TestFromUTF16(t *testing.T) {
	for _, e := range fromUTF16Tests {
		t.Run(e.scenario, func(t *testing.T) {
			m := protocol.NewMapper("", e.input)
			offset, err := m.PositionOffset(protocol.Position{
				Line:      uint32(e.line - 1),
				Character: uint32(e.utf16col - 1),
			})
			if err != nil {
				if err.Error() != e.err {
					t.Fatalf("expected error %v; got %v", e.err, err)
				}
				return
			}
			if e.err != "" {
				t.Fatalf("unexpected success; wanted %v", e.err)
			}
			if offset != e.resOffset {
				t.Fatalf("expected offset %v; got %v", e.resOffset, offset)
			}
			line, col8 := m.OffsetLineCol8(offset)
			if line != e.line {
				t.Fatalf("expected resulting line %v; got %v", e.line, line)
			}
			if col8 != e.resCol {
				t.Fatalf("expected resulting col %v; got %v", e.resCol, col8)
			}
			pre, post := getPrePost(e.input, offset)
			if pre != e.pre {
				t.Fatalf("expected #%d pre %q; got %q", offset, e.pre, pre)
			}
			if post != e.post {
				t.Fatalf("expected #%d post %q; got %q", offset, e.post, post)
			}
		})
	}
}

func getPrePost(content []byte, offset int) (string, string) {
	pre, post := string(content)[:offset], string(content)[offset:]
	if i := strings.LastIndex(pre, "\n"); i >= 0 {
		pre = pre[i+1:]
	}
	if i := strings.IndexRune(post, '\n'); i >= 0 {
		post = post[:i]
	}
	return pre, post
}

// -- these are the historical lsppos tests --

type testCase struct {
	content            string      // input text
	substrOrOffset     interface{} // explicit integer offset, or a substring
	wantLine, wantChar int         // expected LSP position information
}

// offset returns the test case byte offset
func (c testCase) offset() int {
	switch x := c.substrOrOffset.(type) {
	case int:
		return x
	case string:
		i := strings.Index(c.content, x)
		if i < 0 {
			panic(fmt.Sprintf("%q does not contain substring %q", c.content, x))
		}
		return i
	}
	panic("substrOrIndex must be an integer or string")
}

var tests = []testCase{
	{"a𐐀b", "a", 0, 0},
	{"a𐐀b", "𐐀", 0, 1},
	{"a𐐀b", "b", 0, 3},
	{"a𐐀b\n", "\n", 0, 4},
	{"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line.
	{"a𐐀b\r\nx", "x", 1, 0},
	{"a𐐀b\r\nx\ny", "y", 2, 0},

	// Testing EOL and EOF positions
	{"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0)
	{"abc", "c", 0, 2},
	{"abc", 3, 0, 3},
	{"abc\n", "\n", 0, 3},
	{"abc\n", 4, 1, 0}, // position after a newline is on the next line
}

func TestLineChar(t *testing.T) {
	for _, test := range tests {
		m := protocol.NewMapper("", []byte(test.content))
		offset := test.offset()
		posn, _ := m.OffsetPosition(offset)
		gotLine, gotChar := int(posn.Line), int(posn.Character)
		if gotLine != test.wantLine || gotChar != test.wantChar {
			t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar)
		}
	}
}

func TestInvalidOffset(t *testing.T) {
	content := []byte("a𐐀b\r\nx\ny")
	m := protocol.NewMapper("", content)
	for _, offset := range []int{-1, 100} {
		posn, err := m.OffsetPosition(offset)
		if err == nil {
			t.Errorf("OffsetPosition(%d) = %s, want error", offset, posn)
		}
	}
}

func TestPosition(t *testing.T) {
	for _, test := range tests {
		m := protocol.NewMapper("", []byte(test.content))
		offset := test.offset()
		got, err := m.OffsetPosition(offset)
		if err != nil {
			t.Errorf("OffsetPosition(%d) failed: %v", offset, err)
			continue
		}
		want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}
		if got != want {
			t.Errorf("Position(%d) = %v, want %v", offset, got, want)
		}
	}
}

func TestRange(t *testing.T) {
	for _, test := range tests {
		m := protocol.NewMapper("", []byte(test.content))
		offset := test.offset()
		got, err := m.OffsetRange(0, offset)
		if err != nil {
			t.Fatal(err)
		}
		want := protocol.Range{
			End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)},
		}
		if got != want {
			t.Errorf("Range(%d) = %v, want %v", offset, got, want)
		}
	}
}

func TestBytesOffset(t *testing.T) {
	tests := []struct {
		text string
		pos  protocol.Position
		want int
	}{
		// U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16.
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0},
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1},
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1},
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5},
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6},
		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8},
		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1},
		{text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8},
	}

	for i, test := range tests {
		fname := fmt.Sprintf("test %d", i)
		uri := protocol.URIFromPath(fname)
		mapper := protocol.NewMapper(uri, []byte(test.text))
		got, err := mapper.PositionOffset(test.pos)
		if err != nil && test.want != -1 {
			t.Errorf("%d: unexpected error: %v", i, err)
		}
		if err == nil && got != test.want {
			t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got)
		}
	}
}
