package gop_test

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"text/template"
	"time"
	"unsafe"

	"github.com/ysmood/gop"
)

func eq(t *testing.T, a, b interface{}) {
	t.Helper()
	if a == b {
		return
	}
	t.Log(a, "[should equal]", b)
	t.Fail()
}

// RandStr generates a random string with the specified length
func randStr(l int) string {
	b := make([]byte, (l+1)/2)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)[:l]
}

func render(value string, data interface{}) string {
	out := bytes.NewBuffer(nil)
	t := template.New("")
	t, err := t.Parse(value)
	if err != nil {
		panic(err)
	}
	err = t.Execute(out, data)
	if err != nil {
		panic(err)
	}
	return out.String()
}

func assertPanic(t *testing.T, fn func()) (val interface{}) {
	t.Helper()

	defer func() {
		t.Helper()

		val = recover()
		if val == nil {
			t.Error("should panic")
		}
	}()

	fn()

	return
}

func TestStyle(t *testing.T) {
	s := gop.Style{Set: "<s>", Unset: "</s>"}

	eq(t, gop.S("test", s), "<s>test</s>")
	eq(t, gop.S("", s), "<s></s>")
	eq(t, gop.S("", gop.None), "")
}

func TestTokenize(t *testing.T) {
	ref := "test"
	timeStamp, _ := time.Parse(time.RFC3339Nano, "2021-08-28T08:36:36.807908+08:00")
	fn := func(string) int { return 10 }
	ch1 := make(chan int)
	ch2 := make(chan string, 3)
	ch3 := make(chan struct{})

	v := []interface{}{
		nil,
		[]int{},
		[]interface{}{true, false, uintptr(0x17), float32(100.121111133)},
		true, 10, int8(2), int32(100),
		float64(100.121111133),
		complex64(1 + 2i), complex128(1 + 2i),
		[3]int{1, 2},
		ch1,
		ch2,
		ch3,
		fn,
		map[interface{}]interface{}{
			`"test"`: 10,
			"a":      1,
			&ref:     1,
		},
		unsafe.Pointer(&ref),
		struct {
			Int int
			str string
			M   map[int]int
		}{10, "ok", map[int]int{1: 0x20}},
		[]byte("aa\xe2"),
		[]byte("bytes\n\tbytes"),
		[]byte("long long long long string"),
		byte('a'),
		byte(1),
		'天',
		"long long long long string",
		"\ntest",
		"\t\n`",
		&ref,
		(*struct{ Int int })(nil),
		&struct{ Int int }{},
		&map[int]int{1: 2, 3: 4},
		&[]int{1, 2},
		&[2]int{1, 2},
		&[]byte{1, 2},
		timeStamp,
		time.Hour,
		`{"a": 1}`,
		[]byte(`{"a": 1}`),
	}

	check := func(out string, file string) {
		t.Helper()

		tpl, err := os.ReadFile(file)
		if err != nil {
			t.Fatal(err)
		}

		expected := render(string(tpl), map[string]interface{}{
			"ch1": fmt.Sprintf("0x%x", reflect.ValueOf(ch1).Pointer()),
			"ch2": fmt.Sprintf("0x%x", reflect.ValueOf(ch2).Pointer()),
			"ch3": fmt.Sprintf("0x%x", reflect.ValueOf(ch3).Pointer()),
			"fn":  fmt.Sprintf("0x%x", reflect.ValueOf(fn).Pointer()),
			"ptr": fmt.Sprintf("%v", &ref),
			"ref": fmt.Sprintf("0x%x", reflect.ValueOf(&ref).Pointer()),
		})

		if out != expected {
			t.Log("check failed")
			writeFile(t, "tmp/expected.txt", expected)
			writeFile(t, "tmp/out.txt", out)
			t.Fail()
		}
	}

	out := gop.StripANSI(gop.F(v))

	{
		b, err := os.ReadFile(filepath.Join("fixtures", "compile_check.go.tmpl"))
		if err != nil {
			t.Fatal(err)
		}
		code := fmt.Sprintf(string(b), out)
		f := filepath.Join("tmp", randStr(8), "main.go")
		err = os.MkdirAll(filepath.Dir(f), 0755)
		if err != nil {
			t.Fatal(err)
		}
		writeFile(t, f, code)
		b, err = exec.Command("go", "run", f).CombinedOutput()
		if err != nil {
			t.Fatal(string(b))
		}
	}

	check(out, filepath.Join("fixtures", "expected.tmpl"))

	out = gop.VisualizeANSI(gop.F(v))
	check(out, filepath.Join("fixtures", "expected_with_color.tmpl"))
}

func TestRef(t *testing.T) {
	a := [2][]int{{1}}
	a[1] = a[0]

	eq(t, gop.Plain(a), `[2][]int{
    []int{
        1,
    },
    []int{
        1,
    },
}`)
}

type A struct {
	Int int
	B   *B
}

type B struct {
	s string
	a *A
}

func TestCircularRef(t *testing.T) {
	a := A{Int: 10}
	b := B{"test", &a}
	a.B = &b

	eq(t, gop.StripANSI(gop.F(a)), `gop_test.A{
    Int: 10,
    B: &gop_test.B{
        s: "test",
        a: &gop_test.A{
            Int: 10,
            B: gop.Circular("B").(*gop_test.B),
        },
    },
}`)
}

func TestCircularNilRef(t *testing.T) {
	arr := []A{{}, {}}

	eq(t, gop.StripANSI(gop.F(arr)), `[]gop_test.A{
    gop_test.A{
        Int: 0,
        B: (*gop_test.B)(nil),
    },
    gop_test.A{
        Int: 0,
        B: (*gop_test.B)(nil),
    },
}`)
}

func TestCircularMap(t *testing.T) {
	a := map[int]interface{}{}
	a[0] = a

	ts := gop.Tokenize(a)

	eq(t, gop.Format(ts, gop.ThemeNone), `map[int]interface {}{
    0: gop.Circular().(map[int]interface {}),
}`)
}

func TestCircularSlice(t *testing.T) {
	a := [][]interface{}{{nil}, {nil}}
	a[0][0] = a[1]
	a[1][0] = a[0][0]

	ts := gop.Tokenize(a)

	eq(t, gop.Format(ts, gop.ThemeNone), `[][]interface {}{
    []interface {}{
        []interface {}{
            gop.Circular(0, 0).([]interface {}),
        },
    },
    []interface {}{
        gop.Circular(1).([]interface {}),
    },
}`)
}

func TestCircularMapKey(t *testing.T) {
	a := map[interface{}]interface{}{}
	b := map[interface{}]interface{}{}
	a[&b] = b
	b[&a] = a

	ts := gop.Tokenize(b)

	eq(t, gop.Format(ts, gop.ThemeNone), render(`map[interface {}]interface {}{
    (interface {})(nil)/* {{.a}} */: map[interface {}]interface {}{
        (interface {})(nil)/* {{.b}} */: gop.Circular().(map[interface {}]interface {}),
    },
}`, map[string]interface{}{
		"a": fmt.Sprintf("0x%x", reflect.ValueOf(&a).Pointer()),
		"b": fmt.Sprintf("0x%x", reflect.ValueOf(&b).Pointer()),
	}))
}

func TestPlain(t *testing.T) {
	eq(t, gop.Plain(10), "10")
}

func TestPlainMinify(t *testing.T) {
	a := map[int]interface{}{
		1: "a",
	}
	b := map[int]interface{}{}
	pa := gop.Plain(a)
	pb := gop.Plain(b)
	eq(t, pa, "map[int]interface {}{\n    1: \"a\",\n}")
	eq(t, pb, "map[int]interface {}{}")
}

func TestP(t *testing.T) {
	gop.Stdout = io.Discard
	_ = gop.P("test")
	gop.Stdout = os.Stdout
}

func TestConvertors(t *testing.T) {
	eq(t, gop.Circular(""), nil)

	s := randStr(8)
	eq(t, *gop.Ptr(s).(*string), s)

	bs := base64.StdEncoding.EncodeToString([]byte(s))

	eq(t, string(gop.Base64(bs)), string([]byte(s)))
	now := time.Now()
	eq(t, gop.Time(now.Format(time.RFC3339Nano), 1234).Unix(), now.Unix())
	eq(t, gop.Duration("10m"), 10*time.Minute)

	eq(t, gop.JSONStr(nil, "[1, 2]"), "[1, 2]")
	eq(t, string(gop.JSONBytes(nil, "[1, 2]")), string([]byte("[1, 2]")))

	eq(t, gop.Rune(100, 'd'), 'd')
	eq(t, gop.Rune(100, 'd'), int32(100))

	eq(t, gop.GopError("test").Error(), "test")
}

func TestGetPrivateFieldErr(t *testing.T) {
	assertPanic(t, func() {
		gop.GetPrivateField(reflect.ValueOf(1), 0)
	})
	assertPanic(t, func() {
		gop.GetPrivateFieldByName(reflect.ValueOf(1), "test")
	})
}

func TestTypeName(t *testing.T) {
	type f float64
	type i int
	type c complex128
	type b byte

	eq(t, gop.Plain(f(1)), "gop_test.f(1.0)")
	eq(t, gop.Plain(i(1)), "gop_test.i(1)")
	eq(t, gop.Plain(c(1)), "gop_test.c(1+0i)")
	eq(t, gop.Plain(b('a')), "gop_test.b(97)")
}

func TestFixNestedStyle(t *testing.T) {
	s := gop.S(" 0 "+gop.S(" 1 "+
		gop.S(" 2 "+
			gop.S(" 3 ", gop.Cyan)+
			" 4 ", gop.Blue)+
		" 5 ", gop.Red)+" 6 ", gop.BgRed)
	out := gop.VisualizeANSI(gop.FixNestedStyle(s))
	eq(t, out, `<41> 0 <31> 1 <39><34> 2 <39><36> 3 <39><34> 4 <39><31> 5 <39> 6 <49>`)

	gop.FixNestedStyle("test")
}

func TestStripANSI(t *testing.T) {
	eq(t, gop.StripANSI(gop.S("test", gop.Red)), "test")
}

func TestTheme(t *testing.T) {
	eq(t, gop.ThemeDefault(gop.Error)[0], gop.Underline)
}

func TestNil(t *testing.T) {
	eq(t, gop.Plain(map[string]string(nil)), "map[string]string(nil)")
	eq(t, gop.Plain(chan int(nil)), "(chan int)(nil)")
	eq(t, gop.Plain([]string(nil)), "[]string(nil)")
	eq(t, gop.Plain((func())(nil)), "(func())(nil)")
	eq(t, gop.Plain((*struct{})(nil)), "(*struct {})(nil)")
}

func TestJSONArrayBug(t *testing.T) {
	// Test for the bug fix where JSON arrays were incorrectly checked as objects
	// The bug was: _, isArr := jv.(map[string]interface{}) instead of jv.([]interface{})
	// This caused JSON arrays to never be recognized, so they weren't properly formatted

	jsonArrayStr := `[1, 2, 3]`
	result := gop.Plain(jsonArrayStr)

	// With the fix, JSON arrays should be detected and formatted with gop.JSONStr()
	// The result should contain "gop.JSONStr(" indicating proper JSON array detection
	if !strings.Contains(result, "gop.JSONStr(") {
		t.Errorf("JSON array not properly detected as JSON. Got: %s", result)
	}

	// Test with byte array too
	jsonArrayBytes := []byte(`[{"key": "value"}, 123]`)
	result2 := gop.Plain(jsonArrayBytes)

	// With the fix, JSON byte arrays should be detected and formatted with gop.JSONBytes()
	if !strings.Contains(result2, "gop.JSONBytes(") {
		t.Errorf("JSON byte array not properly detected as JSON. Got: %s", result2)
	}

	// Test that the tokenized version contains the actual parsed array structure
	if !strings.Contains(result, "[]interface {}") {
		t.Errorf("JSON array structure not properly tokenized. Got: %s", result)
	}
}

func TestMaxDepth(t *testing.T) {
	// Create a deeply nested structure
	type Node struct {
		Value int
		Next  *Node
	}

	// Create a chain of 20 nodes
	var root *Node
	current := &Node{Value: 0}
	root = current
	for i := 1; i < 20; i++ {
		current.Next = &Node{Value: i}
		current = current.Next
	}

	// Test with default MaxDepth (15)
	result := gop.Plain(root)
	if !strings.Contains(result, "gop.GopError(") {
		t.Errorf("Expected max depth error with default MaxDepth, got: %s", result)
	}
	if !strings.Contains(result, "max depth exceeded") {
		t.Errorf("Expected 'max depth exceeded' message, got: %s", result)
	}

	// Test with custom MaxDepth of 5
	tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 5})
	result = gop.Format(tokens, gop.ThemeNone)
	if !strings.Contains(result, "gop.GopError(") {
		t.Errorf("Expected max depth error with MaxDepth=5, got: %s", result)
	}
	if !strings.Contains(result, "max depth exceeded") {
		t.Errorf("Expected 'max depth exceeded' message with MaxDepth=5, got: %s", result)
	}

	// Test with MaxDepth of 0 (no limit)
	tokens = gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0})
	result = gop.Format(tokens, gop.ThemeNone)
	if strings.Contains(result, "max depth exceeded") {
		t.Errorf("Should not have max depth error with MaxDepth=0, got: %s", result)
	}

	// Verify all 20 values are present when no limit
	for i := 0; i < 20; i++ {
		if !strings.Contains(result, fmt.Sprintf("Value: %d", i)) {
			t.Errorf("Missing Value: %d in unlimited depth output", i)
		}
	}

	// Test with deeply nested maps
	deepMap := map[string]interface{}{
		"level1": map[string]interface{}{
			"level2": map[string]interface{}{
				"level3": map[string]interface{}{
					"level4": map[string]interface{}{
						"level5": map[string]interface{}{
							"level6": "deep value",
						},
					},
				},
			},
		},
	}

	// Test with MaxDepth of 3
	tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 3})
	result = gop.Format(tokens, gop.ThemeNone)
	if !strings.Contains(result, "gop.GopError(") {
		t.Errorf("Expected max depth error with nested maps at MaxDepth=3, got: %s", result)
	}

	// Test with MaxDepth of 10 (should be enough for this structure)
	tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 10})
	result = gop.Format(tokens, gop.ThemeNone)
	if strings.Contains(result, "max depth exceeded") {
		t.Errorf("Should not have max depth error with MaxDepth=10 for this structure, got: %s", result)
	}
	if !strings.Contains(result, "deep value") {
		t.Errorf("Should contain 'deep value' with sufficient MaxDepth, got: %s", result)
	}

	// Test with deeply nested slices
	var deepSlice interface{} = []interface{}{
		[]interface{}{
			[]interface{}{
				[]interface{}{
					[]interface{}{
						[]interface{}{
							"deeply nested",
						},
					},
				},
			},
		},
	}

	tokens = gop.TokenizeWithOptions(deepSlice, gop.Options{MaxDepth: 4})
	result = gop.Format(tokens, gop.ThemeNone)
	if !strings.Contains(result, "gop.GopError(") {
		t.Errorf("Expected max depth error with nested slices at MaxDepth=4, got: %s", result)
	}
}

func writeFile(t *testing.T, f, code string) {
	err := os.WriteFile(f, []byte(code), 0644)
	if err != nil {
		t.Fatal(err)
	}
}

// stringTok is a non-Lit Token that reports String type. It exists so
// tests can exercise the writeReadableString non-Lit branch.
type stringTok struct{ s string }

func (stringTok) Type() gop.Type                 { return gop.String }
func (t stringTok) Build(sb *strings.Builder)    { sb.WriteString(t.s) }

func TestStyledToken(t *testing.T) {
	var sb strings.Builder

	// Nil Inner: Type returns Nil, Build writes nothing.
	empty := gop.Styled{}
	eq(t, empty.Type(), gop.Nil)
	empty.Build(&sb)
	eq(t, sb.String(), "")

	// Non-nil Inner with no active styles: writes inner verbatim.
	sb.Reset()
	plain := gop.Styled{Inner: &gop.Lit{T: gop.String, L: "hi"}}
	eq(t, plain.Type(), gop.String)
	plain.Build(&sb)
	eq(t, sb.String(), "hi")

	// Lit fast path with active style.
	sb.Reset()
	litStyled := gop.Styled{
		Inner:  &gop.Lit{T: gop.String, L: "hi"},
		Styles: []gop.Style{gop.Red},
	}
	litStyled.Build(&sb)
	eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset)

	// Non-Lit inner with active style: goes through the temp builder path.
	sb.Reset()
	nonLitStyled := gop.Styled{
		Inner:  stringTok{s: "hi"},
		Styles: []gop.Style{gop.Red},
	}
	nonLitStyled.Build(&sb)
	eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset)
}

func TestRenderNoStyle(t *testing.T) {
	orig := gop.NoStyle
	gop.NoStyle = true
	defer func() { gop.NoStyle = orig }()

	var sb strings.Builder
	gop.Render(&sb, "hi", []gop.Style{gop.Red})
	eq(t, sb.String(), "hi")
}

func TestRenderMultilineSkipsNone(t *testing.T) {
	var sb strings.Builder
	// Multi-line path with a None entry that must be skipped without altering output.
	gop.Render(&sb, "a\nb", []gop.Style{gop.None, gop.Red})
	eq(t, sb.String(), gop.Red.Set+"a"+gop.Red.Unset+"\n"+gop.Red.Set+"b"+gop.Red.Unset)
}

func TestRenderMultilineCRLF(t *testing.T) {
	// CRLF input exercises the \r-trim branch in Render and the \r\n
	// return in firstNewline; a leading newline also exercises the idx==0
	// segment path.
	var sb strings.Builder
	gop.Render(&sb, "\r\na\r\nb", []gop.Style{gop.Red})
	wrap := func(s string) string { return gop.Red.Set + s + gop.Red.Unset }
	eq(t, sb.String(), wrap("")+"\r\n"+wrap("a")+"\r\n"+wrap("b"))
}

func TestWriteIndentDeep(t *testing.T) {
	// Build a chain of pointers deeper than the indentCache (33 levels)
	// so Format's indent writer falls through to the per-level loop.
	type Node struct{ Next *Node }
	root := &Node{}
	cur := root
	for i := 0; i < 40; i++ {
		cur.Next = &Node{}
		cur = cur.Next
	}

	tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0})
	out := gop.Format(tokens, gop.ThemeNone)
	// The deepest "Next:" field should be indented well beyond 33 levels.
	deepIndent := strings.Repeat("    ", 40) + "Next:"
	if !strings.Contains(out, deepIndent) {
		t.Errorf("expected deep indent past the cache, got:\n%s", out)
	}
}

func TestFormatNonLitStringToken(t *testing.T) {
	// writeReadableString has a non-Lit branch reached only when a
	// caller passes a Token with Type()==String that isn't a *Lit.
	tokens := []gop.Token{stringTok{s: "hi"}}
	eq(t, gop.Format(tokens, gop.ThemeNone), `"hi"`)

	// With an active theme it also exercises the Render path.
	themed := gop.Format(tokens, func(tp gop.Type) []gop.Style {
		if tp == gop.String {
			return []gop.Style{gop.Red}
		}
		return []gop.Style{gop.None}
	})
	eq(t, themed, gop.Red.Set+`"hi"`+gop.Red.Unset)
}

func TestTokenizeNumberAllKinds(t *testing.T) {
	type myInt int
	type myInt8 int8
	type myUint uint
	type myFloat64 float64
	type myComplex128 complex128
	type myUintptr uintptr

	cases := []struct {
		v    interface{}
		want string
	}{
		{int(1), "1"},
		{myInt(1), "gop_test.myInt(1)"},
		{int8(1), "int8(1)"},
		{myInt8(1), "gop_test.myInt8(1)"},
		{uint(1), "uint(1)"},
		{myUint(1), "gop_test.myUint(1)"},
		{uintptr(1), "uintptr(1)"},
		{myUintptr(1), "gop_test.myUintptr(1)"},
		{float32(1), "float32(1)"},
		{float64(1), "1.0"},
		{myFloat64(1), "gop_test.myFloat64(1.0)"},
		{complex64(1 + 2i), "complex64(1+2i)"},
		{complex128(1 + 2i), "1+2i"},
		{myComplex128(1 + 2i), "gop_test.myComplex128(1+2i)"},
	}
	for _, c := range cases {
		got := gop.Plain(c.v)
		if got != c.want {
			t.Errorf("gop.Plain(%v) = %q, want %q", c.v, got, c.want)
		}
	}
}
