//go:build !js && !appengine
// +build !js,!appengine

package runewidth

import (
	"crypto/sha256"
	"fmt"
	"os"
	"sort"
	"testing"
	"unicode/utf8"
)

var _ sort.Interface = (*table)(nil) // ensure that type "table" does implement sort.Interface

func init() {
	os.Setenv("RUNEWIDTH_EASTASIAN", "")
	handleEnv()
}

func (t table) Len() int {
	return len(t)
}

func (t table) Less(i, j int) bool {
	return t[i].first < t[j].first
}

func (t *table) Swap(i, j int) {
	(*t)[i], (*t)[j] = (*t)[j], (*t)[i]
}

type tableInfo struct {
	tbl     table
	name    string
	wantN   int
	wantSHA string
}

var tables = []tableInfo{
	{private, "private", 137468, "a4a641206dc8c5de80bd9f03515a54a706a5a4904c7684dc6a33d65c967a51b2"},
	{nonprint, "nonprint", 2143, "288904683eb225e7c4c0bd3ee481b53e8dace404ec31d443afdbc4d13729fe95"},
	{combining, "combining", 465, "3cce13deb5e23f9f7327f2b1ef162328285a7dcf277a98302a8f7cdd43971268"},
	{doublewidth, "doublewidth", 182440, "3d16eda8650dc2c92d6318d32f0b4a74fda5a278db2d4544b1dd65863394823c"},
	{ambiguous, "ambiguous", 138739, "d05e339a10f296de6547ff3d6c5aee32f627f6555477afebd4a3b7e3cf74c9e3"},
	{emoji, "emoji", 3535, "9ec17351601d49c535658de8d129c1d0ccda2e620669fc39a2faaee7dedcef6d"},
	{narrow, "narrow", 111, "fa897699c5e3cd9141c638d539331b0bdd508b874e22996c5e929767d455fc5a"},
	{neutral, "neutral", 27333, "5455f5e75c307f70b4e9b2384dc5a8bcd91a4c5e2b24b2b185dfad4d860ee5c2"},
}

func TestTableChecksums(t *testing.T) {
	for _, ti := range tables {
		gotN := 0
		buf := make([]byte, utf8.MaxRune+1)
		for r := rune(0); r <= utf8.MaxRune; r++ {
			if inTable(r, ti.tbl) {
				gotN++
				buf[r] = 1
			}
		}
		gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
		if gotN != ti.wantN || gotSHA != ti.wantSHA {
			t.Errorf("table = %s,\n\tn = %d want %d,\n\tsha256 = %s want %s", ti.name, gotN, ti.wantN, gotSHA, ti.wantSHA)
		}
	}
}

func TestRuneWidthChecksums(t *testing.T) {
	testcases := []struct {
		name           string
		eastAsianWidth bool
		wantSHA        string
	}{
		{"ea-no", false, "4eb632b105d3b2c800dda9141381d0b8a95250a3a5c7f1a5ca2c4d4daaa85234"},
		{"ea-yes", true, "c2ddc3bdf42d81d4c23050e21eda46eb639b38b15322d35e8eb6c26f3b83ce92"},
	}

	for _, testcase := range testcases {
		c := NewCondition()
		c.EastAsianWidth = testcase.eastAsianWidth
		buf := make([]byte, utf8.MaxRune+1)
		for r := rune(0); r <= utf8.MaxRune; r++ {
			buf[r] = byte(c.RuneWidth(r))
		}
		gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
		if gotSHA != testcase.wantSHA {
			t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
				testcase.name, gotSHA, testcase.wantSHA)
		}

		// Test with LUT
		c.CreateLUT()
		for r := rune(0); r <= utf8.MaxRune; r++ {
			buf[r] = byte(c.RuneWidth(r))
		}
		gotSHA = fmt.Sprintf("%x", sha256.Sum256(buf))
		if gotSHA != testcase.wantSHA {
			t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
				testcase.name, gotSHA, testcase.wantSHA)
		}
	}
}

func TestDefaultLUT(t *testing.T) {
	testcases := []struct {
		name           string
		eastAsianWidth bool
		wantSHA        string
	}{
		{"ea-no", false, "4eb632b105d3b2c800dda9141381d0b8a95250a3a5c7f1a5ca2c4d4daaa85234"},
		{"ea-yes", true, "c2ddc3bdf42d81d4c23050e21eda46eb639b38b15322d35e8eb6c26f3b83ce92"},
	}

	old := os.Getenv("RUNEWIDTH_EASTASIAN")
	defer os.Setenv("RUNEWIDTH_EASTASIAN", old)

	CreateLUT()
	for _, testcase := range testcases {
		c := DefaultCondition

		if testcase.eastAsianWidth {
			os.Setenv("RUNEWIDTH_EASTASIAN", "1")
		} else {
			os.Setenv("RUNEWIDTH_EASTASIAN", "0")
		}
		handleEnv()

		buf := make([]byte, utf8.MaxRune+1)
		for r := rune(0); r <= utf8.MaxRune; r++ {
			buf[r] = byte(c.RuneWidth(r))
		}
		gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
		if gotSHA != testcase.wantSHA {
			t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
				testcase.name, gotSHA, testcase.wantSHA)
		}
	}
	// Remove for other tests.
	DefaultCondition.combinedLut = nil
}

func checkInterval(first, last rune) bool {
	return first >= 0 && first <= utf8.MaxRune &&
		last >= 0 && last <= utf8.MaxRune &&
		first <= last
}

func isCompact(t *testing.T, ti *tableInfo) bool {
	tbl := ti.tbl
	for i := range tbl {
		e := tbl[i]
		if !checkInterval(e.first, e.last) { // sanity check
			t.Errorf("table invalid: table = %s index = %d %v", ti.name, i, e)
			return false
		}
		if i+1 < len(tbl) && e.last+1 >= tbl[i+1].first { // can be combined into one entry
			t.Errorf("table not compact: table = %s index = %d %v %v", ti.name, i, e, tbl[i+1])
			return false
		}
	}
	return true
}

func TestSorted(t *testing.T) {
	for _, ti := range tables {
		if !sort.IsSorted(&ti.tbl) {
			t.Errorf("table not sorted: %s", ti.name)
		}
		if !isCompact(t, &ti) {
			t.Errorf("table not compact: %s", ti.name)
		}
	}
}

var runewidthtests = []struct {
	in     rune
	out    int
	eaout  int
	nseout int
}{
	{'ä¸–', 2, 2, 2},
	{'ç•Œ', 2, 2, 2},
	{'ï½¾', 1, 1, 1},
	{'ï½¶', 1, 1, 1},
	{'ï½²', 1, 1, 1},
	{'â˜†', 1, 2, 2}, // double width in ambiguous
	{'â˜º', 1, 1, 2},
	{'â˜»', 1, 1, 2},
	{'â™¥', 1, 2, 2},
	{'â™¦', 1, 1, 2},
	{'â™£', 1, 2, 2},
	{'â™ ', 1, 2, 2},
	{'â™‚', 1, 2, 2},
	{'â™€', 1, 2, 2},
	{'â™ª', 1, 2, 2},
	{'â™«', 1, 1, 2},
	{'â˜¼', 1, 1, 2},
	{'â†•', 1, 2, 2},
	{'â€¼', 1, 1, 2},
	{'â†”', 1, 2, 2},
	{'\x00', 0, 0, 0},
	{'\x01', 0, 0, 0},
	{'\u0300', 0, 0, 0},
	{'\u2028', 0, 0, 0},
	{'\u2029', 0, 0, 0},
	{'a', 1, 1, 1}, // ASCII classified as "na" (narrow)
	{'âŸ¦', 1, 1, 1}, // non-ASCII classified as "na" (narrow)
	{'ğŸ‘�', 1, 1, 2},
}

func TestRuneWidth(t *testing.T) {
	c := NewCondition()
	c.EastAsianWidth = false
	for _, tt := range runewidthtests {
		if out := c.RuneWidth(tt.in); out != tt.out {
			t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=false)", tt.in, out, tt.out)
		}
	}
	c.EastAsianWidth = true
	for _, tt := range runewidthtests {
		if out := c.RuneWidth(tt.in); out != tt.eaout {
			t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=true)", tt.in, out, tt.eaout)
		}
	}
	c.StrictEmojiNeutral = false
	for _, tt := range runewidthtests {
		if out := c.RuneWidth(tt.in); out != tt.nseout {
			t.Errorf("RuneWidth(%q) = %d, want %d (StrictEmojiNeutral=false)", tt.in, out, tt.eaout)
		}
	}
}

var isAmbiguousWidthTests = []struct {
	in  rune
	out bool
}{
	{'ä¸–', false},
	{'â– ', true},
	{'ç•Œ', false},
	{'â—‹', true},
	{'ãˆ±', false},
	{'â‘ ', true},
	{'â‘¡', true},
	{'â‘¢', true},
	{'â‘£', true},
	{'â‘¤', true},
	{'â‘¥', true},
	{'â‘¦', true},
	{'â‘§', true},
	{'â‘¨', true},
	{'â‘©', true},
	{'â‘ª', true},
	{'â‘«', true},
	{'â‘¬', true},
	{'â‘­', true},
	{'â‘®', true},
	{'â‘¯', true},
	{'â‘°', true},
	{'â‘±', true},
	{'â‘²', true},
	{'â‘³', true},
	{'â˜†', true},
}

func TestIsAmbiguousWidth(t *testing.T) {
	cond := NewCondition()
	cond.EastAsianWidth = true
	for _, tt := range isAmbiguousWidthTests {
		width := cond.RuneWidth(tt.in)
		if width != 2 {
			t.Fatalf("in=%#v, width=%v", tt.in, width)
		}
		// if out := IsAmbiguousWidth(tt.in); out != tt.out {
		// 		t.Errorf("IsAmbiguousWidth(%q) = %v, want %v", tt.in, out, tt.out)
		// }
	}
}

var stringwidthtests = []struct {
	in    string
	out   int
	eaout int
}{
	{"â– ãˆ±ã�®ä¸–ç•Œâ‘ ", 10, 12},
	{"ã‚¹ã‚¿ãƒ¼â˜†", 7, 8},
	{"ã�¤ã�®ã� â˜†HIRO", 11, 12},
}

func TestStringWidth(t *testing.T) {
	c := NewCondition()
	c.EastAsianWidth = false
	for _, tt := range stringwidthtests {
		if out := c.StringWidth(tt.in); out != tt.out {
			t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.out)
		}
	}
	c.EastAsianWidth = true
	for _, tt := range stringwidthtests {
		if out := c.StringWidth(tt.in); out != tt.eaout {
			t.Errorf("StringWidth(%q) = %d, want %d (EA)", tt.in, out, tt.eaout)
		}
	}
}

func TestStringWidthInvalid(t *testing.T) {
	s := "ã�“ã‚“ã�«ã�¡ã‚�\x00ä¸–ç•Œ"
	if out := StringWidth(s); out != 14 {
		t.Errorf("StringWidth(%q) = %d, want %d", s, out, 14)
	}
}

// var isNeutralWidthTests = []struct {
// 	in  rune
// 	out bool
// }{
// 	{'â†’', false},
// 	{'â”Š', false},
// 	{'â”ˆ', false},
// 	{'ï½�', false},
// 	{'â””', false},
// 	{'â£€', true},
// 	{'â£€', true},
// }

// func TestIsNeutralWidth(t *testing.T) {
// 	c := NewCondition()
// 	for _, tt := range isNeutralWidthTests {
// 		width := c.RuneWidth(tt.in)
// 		t.Log(width, string(tt.in))
// 		// if out := IsNeutralWidth(tt.in); out != tt.out {
// 		// 	t.Errorf("IsNeutralWidth(%q) = %v, want %v", tt.in, out, tt.out)
// 		// }
// 	}
// }

func TestEnv(t *testing.T) {
	old := os.Getenv("RUNEWIDTH_EASTASIAN")
	defer os.Setenv("RUNEWIDTH_EASTASIAN", old)

	os.Setenv("RUNEWIDTH_EASTASIAN", "0")
	handleEnv()

	if w := StringWidth("â”‚"); w != 1 {
		t.Errorf("StringWidth('â”‚') = %d, want %d", w, 1)
	}
}

func TestZeroWidthJoiner(t *testing.T) {
	c := NewCondition()

	tests := []struct {
		in   string
		want int
	}{
		{"ğŸ‘©", 2},
		{"ğŸ‘©\u200d", 2},
		{"ğŸ‘©\u200dğŸ�³", 2},
		{"\u200dğŸ�³", 2},
		{"ğŸ‘¨\u200dğŸ‘¨", 2},
		{"ğŸ‘¨\u200dğŸ‘¨\u200dğŸ‘§", 2},
		{"ã�‚ğŸ‘©\u200dğŸ�³ã�„", 6},
		{"ã�‚\u200dğŸ�³ã�„", 6},
		{"ã�‚\u200dã�„", 4},
	}

	for _, tt := range tests {
		if got := c.StringWidth(tt.in); got != tt.want {
			t.Errorf("StringWidth(%q) = %d, want %d", tt.in, got, tt.want)
		}
	}
}
