package goja

import (
	"strings"
	"testing"
	"unicode/utf16"
)

func TestStringOOBProperties(t *testing.T) {
	const SCRIPT = `
	var string = new String("str");
	
	string[4] = 1;
	string[4];
	`

	testScript(SCRIPT, valueInt(1), t)
}

func TestImportedString(t *testing.T) {
	vm := New()

	testUnaryOp := func(a, expr string, result interface{}, t *testing.T) {
		v, err := vm.RunString("a => " + expr)
		if err != nil {
			t.Fatal(err)
		}
		var fn func(a Value) (Value, error)
		err = vm.ExportTo(v, &fn)
		if err != nil {
			t.Fatal(err)
		}
		for _, aa := range []Value{newStringValue(a), vm.ToValue(a)} {
			res, err := fn(aa)
			if err != nil {
				t.Fatal(err)
			}
			if res.Export() != result {
				t.Fatalf("%s, a:%v(%T). expected: %v, actual: %v", expr, aa, aa, result, res)
			}
		}
	}

	testBinaryOp := func(a, b, expr string, result interface{}, t *testing.T) {
		v, err := vm.RunString("(a, b) => " + expr)
		if err != nil {
			t.Fatal(err)
		}
		var fn func(a, b Value) (Value, error)
		err = vm.ExportTo(v, &fn)
		if err != nil {
			t.Fatal(err)
		}
		for _, aa := range []Value{newStringValue(a), vm.ToValue(a)} {
			for _, bb := range []Value{newStringValue(b), vm.ToValue(b)} {
				res, err := fn(aa, bb)
				if err != nil {
					t.Fatal(err)
				}
				if res.Export() != result {
					t.Fatalf("%s, a:%v(%T), b:%v(%T). expected: %v, actual: %v", expr, aa, aa, bb, bb, result, res)
				}
			}
		}
	}

	strs := []string{"shortAscii", "longlongAscii1234567890123456789", "short юникод", "long юникод 1234567890 юникод \U0001F600", "юникод", "Ascii", "long", "код"}
	indexOfResults := [][]int{
		/*
			const strs = ["shortAscii", "longlongAscii1234567890123456789", "short юникод", "long юникод 1234567890 юникод \u{1F600}", "юникод", "Ascii", "long", "код"];

			strs.forEach(a => {
			    console.log("{", strs.map(b => a.indexOf(b)).join(", "), "},");
			});
		*/
		{0, -1, -1, -1, -1, 5, -1, -1},
		{-1, 0, -1, -1, -1, 8, 0, -1},
		{-1, -1, 0, -1, 6, -1, -1, 9},
		{-1, -1, -1, 0, 5, -1, 0, 8},
		{-1, -1, -1, -1, 0, -1, -1, 3},
		{-1, -1, -1, -1, -1, 0, -1, -1},
		{-1, -1, -1, -1, -1, -1, 0, -1},
		{-1, -1, -1, -1, -1, -1, -1, 0},
	}

	lastIndexOfResults := [][]int{
		/*
			strs.forEach(a => {
			    console.log("{", strs.map(b => a.lastIndexOf(b)).join(", "), "},");
			});
		*/
		{0, -1, -1, -1, -1, 5, -1, -1},
		{-1, 0, -1, -1, -1, 8, 4, -1},
		{-1, -1, 0, -1, 6, -1, -1, 9},
		{-1, -1, -1, 0, 23, -1, 0, 26},
		{-1, -1, -1, -1, 0, -1, -1, 3},
		{-1, -1, -1, -1, -1, 0, -1, -1},
		{-1, -1, -1, -1, -1, -1, 0, -1},
		{-1, -1, -1, -1, -1, -1, -1, 0},
	}

	pad := func(s, p string, n int, start bool) string {
		if n == 0 {
			return s
		}
		if p == "" {
			p = " "
		}
		var b strings.Builder
		ss := utf16.Encode([]rune(s))
		b.Grow(n)
		n -= len(ss)
		if !start {
			b.WriteString(s)
		}
		if n > 0 {
			pp := utf16.Encode([]rune(p))
			for n > 0 {
				if n > len(pp) {
					b.WriteString(p)
					n -= len(pp)
				} else {
					b.WriteString(string(utf16.Decode(pp[:n])))
					n = 0
				}
			}
		}
		if start {
			b.WriteString(s)
		}
		return b.String()
	}

	for i, a := range strs {
		testUnaryOp(a, "JSON.parse(JSON.stringify(a))", a, t)
		testUnaryOp(a, "a.length", int64(len(utf16.Encode([]rune(a)))), t)
		for j, b := range strs {
			testBinaryOp(a, b, "a === b", a == b, t)
			testBinaryOp(a, b, "a == b", a == b, t)
			testBinaryOp(a, b, "a + b", a+b, t)
			testBinaryOp(a, b, "a > b", strings.Compare(a, b) > 0, t)
			testBinaryOp(a, b, "`A${a}B${b}C`", "A"+a+"B"+b+"C", t)
			testBinaryOp(a, b, "a.indexOf(b)", int64(indexOfResults[i][j]), t)
			testBinaryOp(a, b, "a.lastIndexOf(b)", int64(lastIndexOfResults[i][j]), t)
			testBinaryOp(a, b, "a.padStart(32, b)", pad(a, b, 32, true), t)
			testBinaryOp(a, b, "a.padEnd(32, b)", pad(a, b, 32, false), t)
			testBinaryOp(a, b, "a.replace(b, '')", strings.Replace(a, b, "", 1), t)
		}
	}
}

func TestStringFromUTF16(t *testing.T) {
	s := StringFromUTF16([]uint16{})
	if s.Length() != 0 || !s.SameAs(asciiString("")) {
		t.Fatal(s)
	}

	s = StringFromUTF16([]uint16{0xD800})
	if s.Length() != 1 || s.CharAt(0) != 0xD800 {
		t.Fatal(s)
	}

	s = StringFromUTF16([]uint16{'A', 'B'})
	if !s.SameAs(asciiString("AB")) {
		t.Fatal(s)
	}
}

func TestStringBuilder(t *testing.T) {
	t.Run("writeUTF8String-switch", func(t *testing.T) {
		var sb StringBuilder
		sb.WriteUTF8String("Head")
		sb.WriteUTF8String("1ábc")
		if res := sb.String().String(); res != "Head1ábc" {
			t.Fatal(res)
		}
	})
}

func TestUnicodeRepeat(t *testing.T) {
	testCases := []struct {
		name     string
		script   string
		expected string
	}{
		{
			name: "Unicode character only",
			script: `
				var str = "★";
				str.repeat(3);
			`,
			expected: "★★★",
		},
		{
			name: "Mixed unicode",
			script: `
				var str = "a★b★c";
				str.repeat(2);
			`,
			expected: "a★b★ca★b★c",
		},
		{
			name: "Single unicode char repeated once",
			script: `
				var str = "★";
				str.repeat(1);
			`,
			expected: "★",
		},
		{
			name: "Unicode with surrogate pairs",
			script: `
				var str = "𝐀𝐁";
				str.repeat(2);
			`,
			expected: "𝐀𝐁𝐀𝐁",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			vm := New()
			v, err := vm.RunString(tc.script)
			if err != nil {
				t.Fatal(err)
			}
			result := v.String()
			if result != tc.expected {
				t.Fatalf("Expected '%s' but got '%s'. Length expected: %d, got: %d",
					tc.expected, result, len([]rune(tc.expected)), len([]rune(result)))
			}
		})
	}
}

func BenchmarkASCIIConcat(b *testing.B) {
	vm := New()

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_, err := vm.RunString(`{let result = "ab";
		for (let i = 0 ; i < 10;i++) {
			result += result;
		}}`)
		if err != nil {
			b.Fatalf("Unexpected errors %s", err)
		}
	}
}
