package js

import (
	"bytes"
	"io"
	"strings"
	"testing"

	"github.com/tdewolff/parse/v2"
	"github.com/tdewolff/test"
)

func TestJS(t *testing.T) {
	var tests = []struct {
		js       string
		expected string
	}{
		// BlockStmt
		{"if (true) { x(1, 2, 3); };", "if (true) { x(1, 2, 3); }; "},

		// IfStmt
		{"if (true) { true; };", "if (true) { true; }; "},
		{"if (true) { true; } else { false; };", "if (true) { true; } else { false; }; "},
		{"if (true) { true; } else { if(true) { true; } else { false; } };", "if (true) { true; } else { if (true) { true; } else { false; }; }; "},

		// DoWhileStmt
		{"do { continue; } while (true);", "do { continue; } while (true); "},
		{"do { x = 1; } while (true);", "do { x = 1; } while (true); "},

		// WhileStmt
		{"while (true) { true; };", "while (true) { true; }; "},
		{"while (true) { x = 1; };", "while (true) { x = 1; }; "},

		// ForStmt
		{"for ( ; ; ) { true; };", "for ( ; ; ) { true; }; "},
		{"for (x = 1; ; ) { true; };", "for (x = 1; ; ) { true; }; "},
		{"for (x = 1; x < 2; ) { true; };", "for (x = 1; x < 2; ) { true; }; "},
		{"for (x = 1; x < 2; x++) { true; };", "for (x = 1; x < 2; x++) { true; }; "},
		{"for (x = 1; x < 2; x++) { x = 1; };", "for (x = 1; x < 2; x++) { x = 1; }; "},

		// ForInStmt
		{"for (var x in [1, 2]) { true; };", "for (var x in [1, 2]) { true; }; "},
		{"for (var x in [1, 2]) { x = 1; };", "for (var x in [1, 2]) { x = 1; }; "},

		// ForOfStmt
		{"for (const element of [1, 2]) { true; };", "for (const element of [1, 2]) { true; }; "},
		{"for (const element of [1, 2]) { x = 1; };", "for (const element of [1, 2]) { x = 1; }; "},

		// SwitchStmt
		{"switch (true) { case true: break; case false: false; };", "switch (true) { case true: break; case false: false; }; "},
		{"switch (true) { case true: x(); break; case false: x(); false; };", "switch (true) { case true: x(); break; case false: x(); false; }; "},
		{"switch (true) { default: false; };", "switch (true) { default: false; }; "},

		// BranchStmt
		{"for (i = 0; i < 3; i++) { continue; }; ", "for (i = 0; i < 3; i++) { continue; }; "},
		{"for (i = 0; i < 3; i++) { x = 1; }; ", "for (i = 0; i < 3; i++) { x = 1; }; "},

		// ReturnStmt
		{"return;", "return; "},
		{"return 1;", "return 1; "},

		// WithStmt
		{"with (true) { true; };", "with (true) { true; }; "},
		{"with (true) { x = 1; };", "with (true) { x = 1; }; "},

		// LabelledStmt
		{"loop: for (x = 0; x < 1; x++) { true; };", "loop: for (x = 0; x < 1; x++) { true; }; "},

		// ThrowStmt
		{"throw x;", "throw x; "},

		// TryStmt
		{"try { true; } catch(e) { };", "try { true; } catch(e) { }; "},
		{"try { true; } catch(e) { true; };", "try { true; } catch(e) { true; }; "},
		{"try { true; } catch(e) { x = 1; };", "try { true; } catch(e) { x = 1; }; "},

		// DebuggerStmt
		{"debugger;", "debugger; "},

		// Alias
		{"import * as name from 'module-name';", "import * as name from 'module-name'; "},

		// ImportStmt
		{"import defaultExport from 'module-name';", "import defaultExport from 'module-name'; "},
		{"import * as name from 'module-name';", "import * as name from 'module-name'; "},
		{"import { export1 } from 'module-name';", "import { export1 } from 'module-name'; "},
		{"import { export1 as alias1 } from 'module-name';", "import { export1 as alias1 } from 'module-name'; "},
		{"import { export1 , export2 } from 'module-name';", "import { export1 , export2 } from 'module-name'; "},
		{"import { foo , bar } from 'module-name/path/to/specific/un-exported/file';", "import { foo , bar } from 'module-name/path/to/specific/un-exported/file'; "},
		{"import defaultExport, * as name from 'module-name';", "import defaultExport , * as name from 'module-name'; "},
		{"import 'module-name';", "import 'module-name'; "},
		{"var promise = import('module-name');", "var promise = import('module-name'); "},

		// ExportStmt
		{"export { myFunction as default };", "export { myFunction as default }; "},
		{"export default k = 12;", "export default k = 12; "},

		// DirectivePrologueStmt
		{"'use strict';", "'use strict'; "},

		// BindingArray
		{"let [name1, name2 = 6] = z;", "let [name1, name2 = 6] = z; "},

		// BindingObject
		{"let {name1, key2: name2} = z;", "let {name1, key2: name2} = z; "},

		// BindingElement
		{"let [{name: key, ...rest}, ...[c,d=9]] = z;", "let [{name: key, ...rest}, ...[c, d = 9]] = z; "},

		// VarDecl
		{"x = 1;", "x = 1; "},
		{"var x;", "var x; "},
		{"var x = 1;", "var x = 1; "},
		{"var x, y = [];", "var x, y = []; "},
		{"let x;", "let x; "},
		{"let x = 1;", "let x = 1; "},
		{"const x = 1;", "const x = 1; "},

		// Params
		{"function xyz (a, b) { };", "function xyz (a, b) { }; "},
		{"function xyz (a, b, ...c) { };", "function xyz (a, b, ...c) { }; "},

		// FuncDecl
		{"function xyz (a, b) { };", "function xyz (a, b) { }; "},

		// MethodDecl
		{"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "},

		// FieldDefinition
		{"class A { field; };", "class A { field; }; "},
		{"class A { field = 5; };", "class A { field = 5; }; "},

		// ClassDecl
		{"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "},
		{"class B extends A { field; static get method () { }; };", "class B extends A { field; static get method () { }; }; "},

		// LiteralExpr
		{"'test';", "'test'; "},

		// ArrayExpr
		{"[1, 2, 3];", "[1, 2, 3]; "},

		// Property
		{`x = {x: "value"};`, `x = {x: "value"}; `},
		{`x = {"x": "value"};`, `x = {x: "value"}; `},
		{`x = {"1a": 2};`, `x = {"1a": 2}; `},

		// ObjectExpr
		{`x = {x: "value", y: "value"};`, `x = {x: "value", y: "value"}; `},

		// TemplateExpr
		{"x = `value`;", "x = `value`; "},
		{"x = `value${'hi'}`;", "x = `value${'hi'}`; "},

		// GroupExpr
		{"x = (1 + 1) / 1;", "x = (1 + 1) / 1; "},

		// IndexExpr
		{"x = y[1];", "x = y[1]; "},

		// DotExpr
		{"x = y.z;", "x = y.z; "},

		// NewTargetExpr
		{"x = new.target;", "x = new.target; "},

		// ImportMetaExpr
		{"x = import.meta;", "x = import.meta; "},

		// Args
		{"x(1, 2);", "x(1, 2); "},

		// NewExpr
		{"new x;", "new x(); "},
		{"new x(1);", "new x(1); "},
		{"new Date().getTime();", "new Date().getTime(); "},

		// CallExpr
		{"x();", "x(); "},

		// OptChainExpr
		{"x = y?.z;", "x = y?.z; "},

		// UnaryExpr
		{"x = 1 + 1;", "x = 1 + 1; "},

		// BinaryExpr
		{"a << b;", "a << b; "},

		// CondExpr
		{"a && b;", "a && b; "},
		{"a || b;", "a || b; "},

		// YieldExpr
		{"x = function* foo (x) { while (x < 2) { yield x; x++; }; };", "x = function* foo (x) { while (x < 2) { yield x; x++; }; }; "},

		// ArrowFunc
		{"(x) => { y(); };", "(x) => { y(); }; "},
		{"(x, y) => { z(); };", "(x, y) => { z(); }; "},
		{"async (x, y) => { z(); };", "async (x, y) => { z(); }; "},

		// Await keyword
		{"await x;", "await x; "},
		{"export default await x;", "export default await x; "},
		{"export let a = await x;", "export let a = await x; "},
	}
	for _, tt := range tests {
		t.Run(tt.js, func(t *testing.T) {
			ast, err := Parse(parse.NewInputString(tt.js), Options{})
			if err != io.EOF {
				test.Error(t, err)
			}
			test.String(t, ast.JS(), tt.expected)
		})
	}
}
func TestJSWriteTo(t *testing.T) {
	var tests = []struct {
		js       string
		expected string
	}{
		// BlockStmt
		{"if (true) { x(1, 2, 3); };", "if (true) { x(1, 2, 3); }; "},

		// IfStmt
		{"if (true) { true; };", "if (true) { true; }; "},
		{"if (true) { true; } else { false; };", "if (true) { true; } else { false; }; "},
		{"if (true) { true; } else { if(true) { true; } else { false; } };", "if (true) { true; } else { if (true) { true; } else { false; }; }; "},

		// DoWhileStmt
		{"do { continue; } while (true);", "do { continue; } while (true); "},
		{"do { x = 1; } while (true);", "do { x = 1; } while (true); "},

		// WhileStmt
		{"while (true) { true; };", "while (true) { true; }; "},
		{"while (true) { x = 1; };", "while (true) { x = 1; }; "},

		// ForStmt
		{"for ( ; ; ) { true; };", "for ( ; ; ) { true; }; "},
		{"for (x = 1; ; ) { true; };", "for (x = 1; ; ) { true; }; "},
		{"for (x = 1; x < 2; ) { true; };", "for (x = 1; x < 2; ) { true; }; "},
		{"for (x = 1; x < 2; x++) { true; };", "for (x = 1; x < 2; x++) { true; }; "},
		{"for (x = 1; x < 2; x++) { x = 1; };", "for (x = 1; x < 2; x++) { x = 1; }; "},

		// ForInStmt
		{"for (var x in [1, 2]) { true; };", "for (var x in [1, 2]) { true; }; "},
		{"for (var x in [1, 2]) { x = 1; };", "for (var x in [1, 2]) { x = 1; }; "},

		// ForOfStmt
		{"for (const element of [1, 2]) { true; };", "for (const element of [1, 2]) { true; }; "},
		{"for (const element of [1, 2]) { x = 1; };", "for (const element of [1, 2]) { x = 1; }; "},

		// SwitchStmt
		{"switch (true) { case true: break; case false: false; };", "switch (true) { case true: break; case false: false; }; "},
		{"switch (true) { case true: x(); break; case false: x(); false; };", "switch (true) { case true: x(); break; case false: x(); false; }; "},
		{"switch (true) { default: false; };", "switch (true) { default: false; }; "},

		// BranchStmt
		{"for (i = 0; i < 3; i++) { continue; }; ", "for (i = 0; i < 3; i++) { continue; }; "},
		{"for (i = 0; i < 3; i++) { x = 1; }; ", "for (i = 0; i < 3; i++) { x = 1; }; "},

		// ReturnStmt
		{"return;", "return; "},
		{"return 1;", "return 1; "},

		// WithStmt
		{"with (true) { true; };", "with (true) { true; }; "},
		{"with (true) { x = 1; };", "with (true) { x = 1; }; "},

		// LabelledStmt
		{"loop: for (x = 0; x < 1; x++) { true; };", "loop: for (x = 0; x < 1; x++) { true; }; "},

		// ThrowStmt
		{"throw x;", "throw x; "},

		// TryStmt
		{"try { true; } catch(e) { };", "try { true; } catch(e) { }; "},
		{"try { true; } catch(e) { true; };", "try { true; } catch(e) { true; }; "},
		{"try { true; } catch(e) { x = 1; };", "try { true; } catch(e) { x = 1; }; "},

		// DebuggerStmt
		{"debugger;", "debugger; "},

		// Alias
		{"import * as name from 'module-name';", "import * as name from 'module-name'; "},

		// ImportStmt
		{"import defaultExport from 'module-name';", "import defaultExport from 'module-name'; "},
		{"import * as name from 'module-name';", "import * as name from 'module-name'; "},
		{"import { export1 } from 'module-name';", "import { export1 } from 'module-name'; "},
		{"import { export1 as alias1 } from 'module-name';", "import { export1 as alias1 } from 'module-name'; "},
		{"import { export1 , export2 } from 'module-name';", "import { export1 , export2 } from 'module-name'; "},
		{"import { foo , bar } from 'module-name/path/to/specific/un-exported/file';", "import { foo , bar } from 'module-name/path/to/specific/un-exported/file'; "},
		{"import defaultExport, * as name from 'module-name';", "import defaultExport , * as name from 'module-name'; "},
		{"import 'module-name';", "import 'module-name'; "},
		{"var promise = import('module-name');", "var promise = import('module-name'); "},

		// ExportStmt
		{"export { myFunction as default };", "export { myFunction as default }; "},
		{"export default k = 12;", "export default k = 12; "},

		// DirectivePrologueStmt
		{"'use strict';", "'use strict'; "},

		// BindingArray
		{"let [name1, name2 = 6] = z;", "let [name1, name2 = 6] = z; "},

		// BindingObject
		{"let {name1, key2: name2} = z;", "let {name1, key2: name2} = z; "},

		// BindingElement
		{"let [{name: key, ...rest}, ...[c,d=9]] = z;", "let [{name: key, ...rest}, ...[c, d = 9]] = z; "},

		// VarDecl
		{"x = 1;", "x = 1; "},
		{"var x;", "var x; "},
		{"var x = 1;", "var x = 1; "},
		{"var x, y = [];", "var x, y = []; "},
		{"let x;", "let x; "},
		{"let x = 1;", "let x = 1; "},
		{"const x = 1;", "const x = 1; "},

		// Params
		{"function xyz (a, b) { };", "function xyz (a, b) { }; "},
		{"function xyz (a, b, ...c) { };", "function xyz (a, b, ...c) { }; "},

		// FuncDecl
		{"function xyz (a, b) { };", "function xyz (a, b) { }; "},

		// MethodDecl
		{"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "},

		// FieldDefinition
		{"class A { field; };", "class A { field; }; "},
		{"class A { field = 5; };", "class A { field = 5; }; "},

		// ClassDecl
		{"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "},
		{"class B extends A { field; static get method () { }; };", "class B extends A { field; static get method () { }; }; "},

		// LiteralExpr
		{"'test';", "'test'; "},

		// ArrayExpr
		{"[1, 2, 3];", "[1, 2, 3]; "},

		// Property
		{`x = {x: "value"};`, `x = {x: "value"}; `},
		{`x = {"x": "value"};`, `x = {x: "value"}; `},
		{`x = {"1a": 2};`, `x = {"1a": 2}; `},

		// ObjectExpr
		{`x = {x: "value", y: "value"};`, `x = {x: "value", y: "value"}; `},

		// TemplateExpr
		{"x = `value`;", "x = `value`; "},
		{"x = `value${'hi'}`;", "x = `value${'hi'}`; "},

		// GroupExpr
		{"x = (1 + 1) / 1;", "x = (1 + 1) / 1; "},

		// IndexExpr
		{"x = y[1];", "x = y[1]; "},

		// DotExpr
		{"x = y.z;", "x = y.z; "},

		// NewTargetExpr
		{"x = new.target;", "x = new.target; "},

		// ImportMetaExpr
		{"x = import.meta;", "x = import.meta; "},

		// Args
		{"x(1, 2);", "x(1, 2); "},

		// NewExpr
		{"new x;", "new x(); "},
		{"new x(1);", "new x(1); "},
		{"new Date().getTime();", "new Date().getTime(); "},

		// CallExpr
		{"x();", "x(); "},

		// OptChainExpr
		{"x = y?.z;", "x = y?.z; "},

		// UnaryExpr
		{"x = 1 + 1;", "x = 1 + 1; "},

		// BinaryExpr
		{"a << b;", "a << b; "},

		// CondExpr
		{"a && b;", "a && b; "},
		{"a || b;", "a || b; "},

		// YieldExpr
		{"x = function* foo (x) { while (x < 2) { yield x; x++; }; };", "x = function* foo (x) { while (x < 2) { yield x; x++; }; }; "},

		// ArrowFunc
		{"(x) => { y(); };", "(x) => { y(); }; "},
		{"(x, y) => { z(); };", "(x, y) => { z(); }; "},
		{"async (x, y) => { z(); };", "async (x, y) => { z(); }; "},

		// Await keyword
		{"await x;", "await x; "},
		{"export default await x;", "export default await x; "},
		{"export let a = await x;", "export let a = await x; "},
	}
	for _, tt := range tests {
		t.Run(tt.js, func(t *testing.T) {
			ast, err := Parse(parse.NewInputString(tt.js), Options{})
			if err != io.EOF {
				test.Error(t, err)
			}
			var buf strings.Builder
			ast.JSWriteTo(&buf)
			test.String(t, buf.String(), tt.expected)
		})
	}
}

func TestJSRealWorldJS(t *testing.T) {
	js := `
	var _0x34d2=['log','415343ArZKCi','11ItVJMI','98599KfnlVw','139pQCPDx','526583DuLSJk','Hello\x20World!','5823JSTLxZ','543807ONUblA','2uewDkG','146389ygBdVV','2273BZpJsB'];(function(_0x6b2cbe,_0x3fd0f4){var _0xffa95b=_0x3d17;while(!![]){try{var _0x5239dc=-parseInt(_0xffa95b(0x132))+parseInt(_0xffa95b(0x12f))+-parseInt(_0xffa95b(0x131))*-parseInt(_0xffa95b(0x12c))+parseInt(_0xffa95b(0x135))*-parseInt(_0xffa95b(0x12e))+-parseInt(_0xffa95b(0x12d))+-parseInt(_0xffa95b(0x134))+-parseInt(_0xffa95b(0x133))*-parseInt(_0xffa95b(0x12b));if(_0x5239dc===_0x3fd0f4)break;else _0x6b2cbe['push'](_0x6b2cbe['shift']());}catch(_0x12a1ea){_0x6b2cbe['push'](_0x6b2cbe['shift']());}}}(_0x34d2,0x4d4a4));function hi(){var _0x2d5dfd=_0x3d17;console[_0x2d5dfd(0x12a)](_0x2d5dfd(0x130));}function _0x3d17(_0x59c992,_0x5be83e){_0x59c992=_0x59c992-0x12a;var _0x34d208=_0x34d2[_0x59c992];return _0x34d208;}hi();
	`

	ast, err := Parse(parse.NewInputString(js), Options{})
	if err != nil && err != io.EOF {
		t.Fatal(err)
	}

	// reparse to make sure JS is still valid
	_, err = Parse(parse.NewInputString(ast.JS()), Options{})
	if err != nil {
		t.Error("Err: ", err)
	}
}

func TestJSON(t *testing.T) {
	json := `x=[{"key": [2.5, '\r'], '"': -2E+9}, null, false, true, 5.0e-6, "string", 'stri"ng']`

	ast, err := Parse(parse.NewInputString(json), Options{})
	if err != nil {
		t.Fatal(err)
	}

	val := ast.List[0].(*ExprStmt).Value.(*BinaryExpr).Y.(JSONer)
	buf := &bytes.Buffer{}
	if err := val.JSON(buf); err != nil {
		t.Fatal(err)
	}
	test.String(t, buf.String(), `[{"key": [2.5, "\r"], "\"": -2E+9}, null, false, true, 5.0e-6, "string", "stri\"ng"]`)
}
