package encoder_test

import (
	"os"
	"path/filepath"
	"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"
	"gotest.tools/v3/assert"
)

func parseSourceFile(code string) *ast.SourceFile {
	return parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: "/test.ts",
		Path:     "/test.ts",
	}, code, core.ScriptKindTS)
}

func TestDecodeSourceFile_Basic(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let x = 1;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)
	assert.Equal(t, decoded.AsNode().Kind, ast.KindSourceFile)
	assert.Equal(t, decoded.FileName(), "/test.ts")
	assert.Equal(t, decoded.Text(), "let x = 1;")
	assert.Assert(t, decoded.Statements != nil)
	assert.Assert(t, decoded.EndOfFileToken != nil)
}

func TestDecodeSourceFile_Statements(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let a = 1;\nlet b = 2;\nlet c = 3;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)
	assert.Equal(t, len(decoded.Statements.Nodes), 3)
	for i, stmt := range decoded.Statements.Nodes {
		assert.Equal(t, stmt.Kind, ast.KindVariableStatement, "statement %d", i)
	}
}

func TestDecodeSourceFile_VariableDeclaration(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let x = 1;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	varStmt := decoded.Statements.Nodes[0].AsVariableStatement()
	assert.Assert(t, varStmt.DeclarationList != nil)
	declList := varStmt.DeclarationList.AsVariableDeclarationList()
	assert.Assert(t, declList.Declarations != nil)
	assert.Equal(t, len(declList.Declarations.Nodes), 1)

	decl := declList.Declarations.Nodes[0].AsVariableDeclaration()
	assert.Equal(t, decl.Name().Kind, ast.KindIdentifier)
	assert.Equal(t, decl.Name().AsIdentifier().Text, "x")
	assert.Assert(t, decl.Initializer != nil)
	assert.Equal(t, decl.Initializer.Kind, ast.KindNumericLiteral)
	assert.Equal(t, decl.Initializer.AsNumericLiteral().Text, "1")
}

func TestDecodeSourceFile_VariableDeclarationListFlags(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		code     string
		expected ast.NodeFlags
	}{
		{"const", "const x = 1;", ast.NodeFlagsConst},
		{"let", "let x = 1;", ast.NodeFlagsLet},
		{"var", "var x = 1;", ast.NodeFlagsNone},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			sf := parseSourceFile(tt.code)
			buf, _, err := encoder.EncodeSourceFile(sf)
			assert.NilError(t, err)

			decoded, err := encoder.DecodeSourceFile(buf)
			assert.NilError(t, err)

			declList := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList()
			got := declList.Flags & (ast.NodeFlagsLet | ast.NodeFlagsConst)
			assert.Equal(t, got, tt.expected, "flags for %q: got %d, want %d", tt.code, got, tt.expected)
		})
	}
}

func TestDecodeSourceFile_FunctionDeclaration(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("function add(a: number, b: number): number { return a + b; }")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
	assert.Assert(t, funcDecl.Name() != nil)
	assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "add")
	assert.Assert(t, funcDecl.Parameters != nil)
	assert.Equal(t, len(funcDecl.Parameters.Nodes), 2)
	assert.Assert(t, funcDecl.Type != nil)
	assert.Assert(t, funcDecl.Body != nil)

	param0 := funcDecl.Parameters.Nodes[0].AsParameterDeclaration()
	assert.Equal(t, param0.Name().AsIdentifier().Text, "a")
	assert.Assert(t, param0.Type != nil)
}

func TestDecodeSourceFile_ImportDeclaration(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile(`import { bar } from "bar";`)
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	imp := decoded.Statements.Nodes[0].AsImportDeclaration()
	assert.Assert(t, imp.ImportClause != nil)
	assert.Assert(t, imp.ModuleSpecifier != nil)
	assert.Equal(t, imp.ModuleSpecifier.AsStringLiteral().Text, "bar")

	clause := imp.ImportClause.AsImportClause()
	assert.Assert(t, clause.NamedBindings != nil)
	namedImports := clause.NamedBindings.AsNamedImports()
	assert.Assert(t, namedImports.Elements != nil)
	assert.Equal(t, len(namedImports.Elements.Nodes), 1)
	spec := namedImports.Elements.Nodes[0].AsImportSpecifier()
	assert.Equal(t, spec.Name().AsIdentifier().Text, "bar")
}

func TestDecodeSourceFile_IfStatement(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("if (true) { } else { }")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	ifStmt := decoded.Statements.Nodes[0].AsIfStatement()
	assert.Assert(t, ifStmt.Expression != nil)
	assert.Assert(t, ifStmt.ThenStatement != nil)
	assert.Assert(t, ifStmt.ElseStatement != nil)
	assert.Equal(t, ifStmt.ThenStatement.Kind, ast.KindBlock)
	assert.Equal(t, ifStmt.ElseStatement.Kind, ast.KindBlock)
}

func TestDecodeSourceFile_TemplateExpression(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let x = `hello ${name} world`;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	varDecl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
	tmplExpr := varDecl.Initializer.AsTemplateExpression()
	assert.Assert(t, tmplExpr.Head != nil)
	assert.Equal(t, tmplExpr.Head.AsTemplateHead().Text, "hello ")
	assert.Assert(t, tmplExpr.TemplateSpans != nil)
	assert.Equal(t, len(tmplExpr.TemplateSpans.Nodes), 1)

	span := tmplExpr.TemplateSpans.Nodes[0].AsTemplateSpan()
	assert.Assert(t, span.Expression != nil)
	assert.Equal(t, span.Expression.Kind, ast.KindIdentifier)
	assert.Assert(t, span.Literal != nil)
	assert.Equal(t, span.Literal.AsTemplateTail().Text, " world")
}

func TestDecodeSourceFile_ExportModifier(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("export function foo() {}")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
	assert.Assert(t, funcDecl.Modifiers() != nil)
	assert.Equal(t, len(funcDecl.Modifiers().Nodes), 1)
	assert.Equal(t, funcDecl.Modifiers().Nodes[0].Kind, ast.KindExportKeyword)
}

func TestDecodeSourceFile_Positions(t *testing.T) {
	t.Parallel()
	code := "let x = 1;"
	sf := parseSourceFile(code)
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	assert.Equal(t, decoded.AsNode().Pos(), 0)
	assert.Equal(t, decoded.AsNode().End(), len(code))
}

func TestDecodeSourceFile_ClassDeclaration(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("class Foo { bar(): void {} }")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	classDecl := decoded.Statements.Nodes[0].AsClassDeclaration()
	assert.Assert(t, classDecl.Name() != nil)
	assert.Equal(t, classDecl.Name().AsIdentifier().Text, "Foo")
	assert.Assert(t, classDecl.Members != nil)
	assert.Equal(t, len(classDecl.Members.Nodes), 1)
	assert.Equal(t, classDecl.Members.Nodes[0].Kind, ast.KindMethodDeclaration)
}

func TestDecodeNodes_SubtreeRoundTrip(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("function greet(name: string) { return `Hello, ${name}!`; }")

	var funcNode *ast.Node
	visitor := &ast.NodeVisitor{}
	visitor.Visit = func(node *ast.Node) *ast.Node {
		if node.Kind == ast.KindFunctionDeclaration && funcNode == nil {
			funcNode = node
		}
		return node
	}
	visitor.VisitEachChild(sf.AsNode())
	assert.Assert(t, funcNode != nil)

	buf, _, err := encoder.EncodeNode(funcNode, sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeNodes(buf)
	assert.NilError(t, err)

	assert.Equal(t, decoded.Kind, ast.KindFunctionDeclaration)
	funcDecl := decoded.AsFunctionDeclaration()
	assert.Assert(t, funcDecl.Name() != nil)
	assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "greet")
	assert.Assert(t, funcDecl.Parameters != nil)
	assert.Equal(t, len(funcDecl.Parameters.Nodes), 1)
	assert.Assert(t, funcDecl.Body != nil)
}

func TestDecodeSourceFile_BinaryExpression(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let x = 1 + 2;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
	binExpr := decl.Initializer.AsBinaryExpression()
	assert.Assert(t, binExpr.Left != nil)
	assert.Assert(t, binExpr.Right != nil)
	assert.Assert(t, binExpr.OperatorToken != nil)
	assert.Equal(t, binExpr.Left.Kind, ast.KindNumericLiteral)
	assert.Equal(t, binExpr.Right.Kind, ast.KindNumericLiteral)
}

func TestDecodeSourceFile_KeywordExpressions(t *testing.T) {
	t.Parallel()
	// "this" must decode as KeywordExpression, not Token, or the printer panics
	sf := parseSourceFile("const x = this;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	// Navigate: const x = this -> VariableStatement -> declaration -> initializer
	decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
	thisExpr := decl.Initializer
	assert.Equal(t, thisExpr.Kind, ast.KindThisKeyword)
	// This would panic if decoded as Token instead of KeywordExpression
	assert.Assert(t, thisExpr.AsKeywordExpression() != nil)
}

func TestDecodeSourceFile_EmptyModuleBlock(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("namespace N { }")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	// Navigate: namespace N { } -> ModuleDeclaration -> ModuleBlock
	mod := decoded.Statements.Nodes[0].AsModuleDeclaration()
	assert.Assert(t, mod.Body != nil)
	block := mod.Body.AsModuleBlock()
	// Statements must be non-nil even when empty, otherwise the printer panics
	assert.Assert(t, block.Statements != nil)
	assert.Equal(t, len(block.Statements.Nodes), 0)
}

func TestDecodeSourceFile_EmptyBlockAndParams(t *testing.T) {
	t.Parallel()
	// Empty blocks and parameter lists must decode with non-nil NodeLists (not nil),
	// matching parser behavior. Previously the decoder left them nil, crashing the printer.
	sf := parseSourceFile("function foo() {}")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
	assert.Assert(t, funcDecl.Parameters != nil, "FunctionDeclaration.Parameters must be non-nil for foo()")
	assert.Equal(t, len(funcDecl.Parameters.Nodes), 0)
	assert.Assert(t, funcDecl.Body != nil)
	block := funcDecl.Body.AsBlock()
	assert.Assert(t, block.Statements != nil, "Block.Statements must be non-nil for empty blocks")
	assert.Equal(t, len(block.Statements.Nodes), 0)
}

func TestDecodeSourceFile_ArrowFunctionEmptyParams(t *testing.T) {
	t.Parallel()
	// `() => {}` must decode with non-nil Parameters (empty NodeList),
	// matching parser behavior. Previously the decoder left it nil, crashing the printer.
	sf := parseSourceFile("const f = () => {};")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
	arrow := decl.Initializer.AsArrowFunction()
	assert.Assert(t, arrow.Parameters != nil, "ArrowFunction.Parameters must be non-nil for () => {}")
	assert.Equal(t, len(arrow.Parameters.Nodes), 0)
	assert.Assert(t, arrow.Body != nil)
	block := arrow.Body.AsBlock()
	assert.Assert(t, block.Statements != nil, "Block.Statements must be non-nil for empty body")
	assert.Equal(t, len(block.Statements.Nodes), 0)
}

func TestDecodeSourceFile_FunctionExpressionEmptyParams(t *testing.T) {
	t.Parallel()
	// `function() {}` must decode with non-nil Parameters (empty NodeList).
	sf := parseSourceFile("const f = function() {};")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
	funcExpr := decl.Initializer.AsFunctionExpression()
	assert.Assert(t, funcExpr.Parameters != nil, "FunctionExpression.Parameters must be non-nil for function() {}")
	assert.Equal(t, len(funcExpr.Parameters.Nodes), 0)
}

func TestDecodeSourceFile_PostfixUnaryOperator(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let i = 0; i++;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
	postfix := exprStmt.Expression.AsPostfixUnaryExpression()
	assert.Equal(t, postfix.Operator, ast.KindPlusPlusToken)
	assert.Equal(t, postfix.Operand.Kind, ast.KindIdentifier)
}

func TestDecodeSourceFile_PrefixUnaryOperator(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let x = true; !x;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
	prefix := exprStmt.Expression.AsPrefixUnaryExpression()
	assert.Equal(t, prefix.Operator, ast.KindExclamationToken)
	assert.Equal(t, prefix.Operand.Kind, ast.KindIdentifier)
}

func TestDecodeSourceFile_PostfixDecrement(t *testing.T) {
	t.Parallel()
	sf := parseSourceFile("let n = 5; n--;")
	buf, _, err := encoder.EncodeSourceFile(sf)
	assert.NilError(t, err)

	decoded, err := encoder.DecodeSourceFile(buf)
	assert.NilError(t, err)

	exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
	postfix := exprStmt.Expression.AsPostfixUnaryExpression()
	assert.Equal(t, postfix.Operator, ast.KindMinusMinusToken)
}

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

	buf, _, err := encoder.EncodeSourceFile(sourceFile)
	assert.NilError(b, err)

	b.Run("parse", func(b *testing.B) {
		for b.Loop() {
			parser.ParseSourceFile(ast.SourceFileParseOptions{
				FileName: "/checker.ts",
				Path:     "/checker.ts",
			}, code, core.ScriptKindTS)
		}
	})

	b.Run("decode", func(b *testing.B) {
		for b.Loop() {
			_, decodeErr := encoder.DecodeSourceFile(buf)
			assert.NilError(b, decodeErr)
		}
	})
}
