package encoder_test

import (
	"encoding/binary"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/microsoft/typescript-go/internal/api/encoder"
	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/parser"
	"github.com/microsoft/typescript-go/internal/repo"
	"github.com/microsoft/typescript-go/internal/testutil/baseline"
	"gotest.tools/v3/assert"
)

func TestEncodeSourceFile(t *testing.T) {
	t.Parallel()
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/test.ts",
		Path:     "/test.ts",
	}, "import { bar } from \"bar\";\nexport function foo<T, U>(a: string, b: string): any {}\nfoo();", core.ScriptKindTS)
	t.Run("baseline", func(t *testing.T) {
		t.Parallel()
		buf, _, err := encoder.EncodeSourceFile(sourceFile)
		assert.NilError(t, err)

		str := formatEncodedSourceFile(buf)
		baseline.Run(t, "encodeSourceFile.txt", str, baseline.Options{
			Subfolder: "api",
		})
	})
}

func TestEncodeSourceFileWithUnicodeEscapes(t *testing.T) {
	t.Parallel()
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/test.ts",
		Path:     "/test.ts",
	}, `let a = "😃"; let b = "\ud83d\ude03"; let c = "\udc00\ud83d\ude03"; let d = "\ud83d\ud83d\ude03"`, core.ScriptKindTS)
	t.Run("baseline", func(t *testing.T) {
		t.Parallel()
		buf, _, err := encoder.EncodeSourceFile(sourceFile)
		assert.NilError(t, err)

		str := formatEncodedSourceFile(buf)
		baseline.Run(t, "encodeSourceFileWithUnicodeEscapes.txt", str, baseline.Options{
			Subfolder: "api",
		})
	})
}

func TestBuildNodeIndexTableMatchesEncode(t *testing.T) {
	t.Parallel()
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/test.ts",
		Path:     "/test.ts",
	}, "import { bar } from \"bar\";\nexport function foo<T, U>(a: string, b: string): any {}\nfoo();", core.ScriptKindTS)

	_, encodeTable, err := encoder.EncodeSourceFile(sourceFile)
	assert.NilError(t, err)

	buildTable := encoder.BuildNodeIndexTable(sourceFile)

	// Both tables should produce identical Nodes slices
	assert.Equal(t, len(buildTable.Nodes), len(encodeTable.Nodes), "Nodes slice length mismatch")

	// Every index should map to the same node
	for i := range encodeTable.Nodes {
		assert.Equal(t, buildTable.Nodes[i], encodeTable.Nodes[i], "node mismatch at index %d", i)
	}

	// GetIndex on both tables should agree for every non-nil node
	for i, node := range encodeTable.Nodes {
		if node == nil {
			continue
		}
		encIdx := encodeTable.GetIndex(node)
		buildIdx := buildTable.GetIndex(node)
		assert.Equal(t, encIdx, uint32(i), "encodeTable.GetIndex mismatch at index %d, node kind=%s", i, node.Kind.String())
		assert.Equal(t, buildIdx, encIdx, "buildTable.GetIndex mismatch for node kind=%s", node.Kind.String())
	}
}

func BenchmarkEncodeSourceFile(b *testing.B) {
	repo.SkipIfNoTypeScriptSubmodule(b)
	filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
	fileContent, err := os.ReadFile(filePath)
	assert.NilError(b, err)
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/checker.ts",
		Path:     "/checker.ts",
	}, string(fileContent), core.ScriptKindTS)

	for b.Loop() {
		_, _, err := encoder.EncodeSourceFile(sourceFile)
		assert.NilError(b, err)
	}
}

func BenchmarkBuildNodeIndexTable(b *testing.B) {
	repo.SkipIfNoTypeScriptSubmodule(b)
	filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
	fileContent, err := os.ReadFile(filePath)
	assert.NilError(b, err)
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/checker.ts",
		Path:     "/checker.ts",
	}, string(fileContent), core.ScriptKindTS)

	for b.Loop() {
		encoder.BuildNodeIndexTable(sourceFile)
	}
}

func readUint32(buf []byte, offset int) uint32 {
	return binary.LittleEndian.Uint32(buf[offset : offset+4])
}

func formatEncodedSourceFile(encoded []byte) string {
	var result strings.Builder
	var getIndent func(parentIndex uint32) string
	offsetNodes := readUint32(encoded, encoder.HeaderOffsetNodes)
	offsetStringOffsets := readUint32(encoded, encoder.HeaderOffsetStringOffsets)
	offsetStrings := readUint32(encoded, encoder.HeaderOffsetStringData)
	getIndent = func(parentIndex uint32) string {
		if parentIndex == 0 {
			return ""
		}
		return "  " + getIndent(readUint32(encoded, int(offsetNodes)+int(parentIndex)*encoder.NodeSize+encoder.NodeOffsetParent))
	}
	j := 1
	for i := int(offsetNodes) + encoder.NodeSize; i < len(encoded); i += encoder.NodeSize {
		kind := readUint32(encoded, i+encoder.NodeOffsetKind)
		pos := readUint32(encoded, i+encoder.NodeOffsetPos)
		end := readUint32(encoded, i+encoder.NodeOffsetEnd)
		parentIndex := readUint32(encoded, i+encoder.NodeOffsetParent)
		result.WriteString(getIndent(parentIndex))
		if kind == encoder.SyntaxKindNodeList {
			result.WriteString("NodeList")
		} else {
			result.WriteString(ast.Kind(kind).String())
		}
		data := readUint32(encoded, i+encoder.NodeOffsetData)
		dataType := data & encoder.NodeDataTypeMask
		if ast.Kind(kind) == ast.KindIdentifier || (dataType == encoder.NodeDataTypeString) {
			stringIndex := data & encoder.NodeDataStringIndexMask
			strStart := readUint32(encoded, int(offsetStringOffsets+stringIndex*4))
			strEnd := readUint32(encoded, int(offsetStringOffsets+stringIndex*4)+4)
			str := string(encoded[offsetStrings+strStart : offsetStrings+strEnd])
			result.WriteString(fmt.Sprintf(" \"%s\"", str))
		}
		fmt.Fprintf(&result, " [%d, %d), i=%d, next=%d", pos, end, j, encoded[i+encoder.NodeOffsetNext])
		result.WriteString("\n")
		j++
	}
	return result.String()
}
