package parser

import (
	"bytes"
	"encoding/json"
	"reflect"
	"strings"
	"testing"

	"github.com/projectdiscovery/goja/ast"
)

func marshal(name string, children ...interface{}) interface{} {
	if len(children) == 1 {
		if name == "" {
			return testMarshalNode(children[0])
		}
		return map[string]interface{}{
			name: children[0],
		}
	}
	map_ := map[string]interface{}{}
	length := len(children) / 2
	for i := 0; i < length; i++ {
		name := children[i*2].(string)
		value := children[i*2+1]
		map_[name] = value
	}
	if name == "" {
		return map_
	}
	return map[string]interface{}{
		name: map_,
	}
}

func testMarshalNode(node interface{}) interface{} {
	switch node := node.(type) {

	// Expression

	case *ast.ArrayLiteral:
		return marshal("Array", testMarshalNode(node.Value))

	case *ast.AssignExpression:
		return marshal("Assign",
			"Left", testMarshalNode(node.Left),
			"Right", testMarshalNode(node.Right),
		)

	case *ast.BinaryExpression:
		return marshal("BinaryExpression",
			"Operator", node.Operator.String(),
			"Left", testMarshalNode(node.Left),
			"Right", testMarshalNode(node.Right),
		)

	case *ast.BooleanLiteral:
		return marshal("Literal", node.Value)

	case *ast.CallExpression:
		return marshal("Call",
			"Callee", testMarshalNode(node.Callee),
			"ArgumentList", testMarshalNode(node.ArgumentList),
		)

	case *ast.ConditionalExpression:
		return marshal("Conditional",
			"Test", testMarshalNode(node.Test),
			"Consequent", testMarshalNode(node.Consequent),
			"Alternate", testMarshalNode(node.Alternate),
		)

	case *ast.DotExpression:
		return marshal("Dot",
			"Left", testMarshalNode(node.Left),
			"Member", node.Identifier.Name,
		)

	case *ast.NewExpression:
		return marshal("New",
			"Callee", testMarshalNode(node.Callee),
			"ArgumentList", testMarshalNode(node.ArgumentList),
		)

	case *ast.NullLiteral:
		return marshal("Literal", nil)

	case *ast.NumberLiteral:
		return marshal("Literal", node.Value)

	case *ast.ObjectLiteral:
		return marshal("Object", testMarshalNode(node.Value))

	case *ast.RegExpLiteral:
		return marshal("Literal", node.Literal)

	case *ast.StringLiteral:
		return marshal("Literal", node.Literal)

	case *ast.Binding:
		return marshal("Binding", "Target", testMarshalNode(node.Target),
			"Initializer", testMarshalNode(node.Initializer))

	// Statement

	case *ast.Program:
		return testMarshalNode(node.Body)

	case *ast.BlockStatement:
		return marshal("BlockStatement", testMarshalNode(node.List))

	case *ast.EmptyStatement:
		return "EmptyStatement"

	case *ast.ExpressionStatement:
		return testMarshalNode(node.Expression)

	case *ast.ForInStatement:
		return marshal("ForIn",
			"Into", testMarshalNode(node.Into),
			"Source", marshal("", node.Source),
			"Body", marshal("", node.Body),
		)

	case *ast.FunctionLiteral:
		return marshal("Function", testMarshalNode(node.Body))

	case *ast.Identifier:
		return marshal("Identifier", node.Name)

	case *ast.IfStatement:
		if_ := marshal("",
			"Test", testMarshalNode(node.Test),
			"Consequent", testMarshalNode(node.Consequent),
		).(map[string]interface{})
		if node.Alternate != nil {
			if_["Alternate"] = testMarshalNode(node.Alternate)
		}
		return marshal("If", if_)

	case *ast.LabelledStatement:
		return marshal("Label",
			"Name", node.Label.Name,
			"Statement", testMarshalNode(node.Statement),
		)
	case *ast.PropertyKeyed:
		return marshal("",
			"Key", node.Key,
			"Value", testMarshalNode(node.Value),
		)

	case *ast.ReturnStatement:
		return marshal("Return", testMarshalNode(node.Argument))

	case *ast.SequenceExpression:
		return marshal("Sequence", testMarshalNode(node.Sequence))

	case *ast.ThrowStatement:
		return marshal("Throw", testMarshalNode(node.Argument))

	case *ast.VariableStatement:
		return marshal("Var", testMarshalNode(node.List))

	// Special
	case *ast.ForDeclaration:
		return marshal("For-Into-Decl", testMarshalNode(node.Target))

	case *ast.ForIntoVar:
		return marshal("For-Into-Var", testMarshalNode(node.Binding))

	}

	{
		value := reflect.ValueOf(node)
		if value.Kind() == reflect.Slice {
			tmp0 := []interface{}{}
			for index := 0; index < value.Len(); index++ {
				tmp0 = append(tmp0, testMarshalNode(value.Index(index).Interface()))
			}
			return tmp0
		}
	}

	return nil
}

func testMarshal(node interface{}) string {
	value, err := json.Marshal(testMarshalNode(node))
	if err != nil {
		panic(err)
	}
	return string(value)
}

func TestParserAST(t *testing.T) {
	tt(t, func() {

		test := func(inputOutput string) {
			match := matchBeforeAfterSeparator.FindStringIndex(inputOutput)
			input := strings.TrimSpace(inputOutput[0:match[0]])
			wantOutput := strings.TrimSpace(inputOutput[match[1]:])
			_, program, err := testParse(input)
			is(err, nil)
			haveOutput := testMarshal(program)
			tmp0, tmp1 := bytes.Buffer{}, bytes.Buffer{}
			json.Indent(&tmp0, []byte(haveOutput), "\t\t", "   ")
			json.Indent(&tmp1, []byte(wantOutput), "\t\t", "   ")
			is("\n\t\t"+tmp0.String(), "\n\t\t"+tmp1.String())
		}

		test(`
        ---
[]
        `)

		test(`
        ;
        ---
[
  "EmptyStatement"
]
        `)

		test(`
        ;;;
        ---
[
  "EmptyStatement",
  "EmptyStatement",
  "EmptyStatement"
]
        `)

		test(`
        1; true; abc; "abc"; null;
        ---
[
  {
    "Literal": 1
  },
  {
    "Literal": true
  },
  {
    "Identifier": "abc"
  },
  {
    "Literal": "\"abc\""
  },
  {
    "Literal": null
  }
]
        `)

		test(`
        { 1; null; 3.14159; ; }
        ---
[
  {
    "BlockStatement": [
      {
        "Literal": 1
      },
      {
        "Literal": null
      },
      {
        "Literal": 3.14159
      },
      "EmptyStatement"
    ]
  }
]
        `)

		test(`
        new abc();
        ---
[
  {
    "New": {
      "ArgumentList": [],
      "Callee": {
        "Identifier": "abc"
      }
    }
  }
]
        `)

		test(`
        new abc(1, 3.14159)
        ---
[
  {
    "New": {
      "ArgumentList": [
        {
          "Literal": 1
        },
        {
          "Literal": 3.14159
        }
      ],
      "Callee": {
        "Identifier": "abc"
      }
    }
  }
]
        `)

		test(`
        true ? false : true
        ---
[
  {
    "Conditional": {
      "Alternate": {
        "Literal": true
      },
      "Consequent": {
        "Literal": false
      },
      "Test": {
        "Literal": true
      }
    }
  }
]
        `)

		test(`
        true || false
        ---
[
  {
    "BinaryExpression": {
      "Left": {
        "Literal": true
      },
      "Operator": "||",
      "Right": {
        "Literal": false
      }
    }
  }
]
        `)

		test(`
        0 + { abc: true }
        ---
[
  {
    "BinaryExpression": {
      "Left": {
        "Literal": 0
      },
      "Operator": "+",
      "Right": {
        "Object": [
          {
            "Key": {
               "Idx": 7,
               "Literal": "abc",
               "Value": "abc"
            },
			"Value": {
              "Literal": true
            }
          }
        ]
      }
    }
  }
]
        `)

		test(`
        1 == "1"
        ---
[
  {
    "BinaryExpression": {
      "Left": {
        "Literal": 1
      },
      "Operator": "==",
      "Right": {
        "Literal": "\"1\""
      }
    }
  }
]
        `)

		test(`
        abc(1)
        ---
[
  {
    "Call": {
      "ArgumentList": [
        {
          "Literal": 1
        }
      ],
      "Callee": {
        "Identifier": "abc"
      }
    }
  }
]
        `)

		test(`
        Math.pow(3, 2)
        ---
[
  {
    "Call": {
      "ArgumentList": [
        {
          "Literal": 3
        },
        {
          "Literal": 2
        }
      ],
      "Callee": {
        "Dot": {
          "Left": {
            "Identifier": "Math"
          },
          "Member": "pow"
        }
      }
    }
  }
]
        `)

		test(`
        1, 2, 3
        ---
[
  {
    "Sequence": [
      {
        "Literal": 1
      },
      {
        "Literal": 2
      },
      {
        "Literal": 3
      }
    ]
  }
]
        `)

		test(`
        / abc /gim;
        ---
[
  {
    "Literal": "/ abc /gim"
  }
]
        `)

		test(`
        if (0)
            1;
        ---
[
  {
    "If": {
      "Consequent": {
        "Literal": 1
      },
      "Test": {
        "Literal": 0
      }
    }
  }
]
        `)

		test(`
        0+function(){
            return;
        }
        ---
[
  {
    "BinaryExpression": {
      "Left": {
        "Literal": 0
      },
      "Operator": "+",
      "Right": {
        "Function": {
          "BlockStatement": [
            {
              "Return": null
            }
          ]
        }
      }
    }
  }
]
        `)

		test(`
        xyzzy // Ignore it
        // Ignore this
        // And this
        /* And all..



        ... of this!
        */
        "Nothing happens."
        // And finally this
        ---
[
  {
    "Identifier": "xyzzy"
  },
  {
    "Literal": "\"Nothing happens.\""
  }
]
        `)

		test(`
        ((x & (x = 1)) !== 0)
        ---
[
  {
    "BinaryExpression": {
      "Left": {
        "BinaryExpression": {
          "Left": {
            "Identifier": "x"
          },
          "Operator": "\u0026",
          "Right": {
            "Assign": {
              "Left": {
                "Identifier": "x"
              },
              "Right": {
                "Literal": 1
              }
            }
          }
        }
      },
      "Operator": "!==",
      "Right": {
        "Literal": 0
      }
    }
  }
]
        `)

		test(`
        { abc: 'def' }
        ---
[
  {
    "BlockStatement": [
      {
        "Label": {
          "Name": "abc",
          "Statement": {
            "Literal": "'def'"
          }
        }
      }
    ]
  }
]
        `)

		test(`
        // This is not an object, this is a string literal with a label!
        ({ abc: 'def' })
        ---
[
  {
    "Object": [
      {
		"Key": {
		   "Idx": 77,
		   "Literal": "abc",
		   "Value": "abc"
		},
		"Value": {
          "Literal": "'def'"
        }
      }
    ]
  }
]
        `)

		test(`
        [,]
        ---
[
  {
    "Array": [
      null
    ]
  }
]
        `)

		test(`
        [,,]
        ---
[
  {
    "Array": [
      null,
      null
    ]
  }
]
        `)

		test(`
        ({ get abc() {} })
        ---
[
  {
    "Object": [
      {
		"Key": {
		   "Idx": 8,
		   "Literal": "abc",
		   "Value": "abc"
		},
        "Value": {
          "Function": {
            "BlockStatement": []
          }
        }
      }
    ]
  }
]
        `)

		test(`
        /abc/.source
        ---
[
  {
    "Dot": {
      "Left": {
        "Literal": "/abc/"
      },
      "Member": "source"
    }
  }
]
        `)

		test(`
                xyzzy

        throw new TypeError("Nothing happens.")
        ---
[
  {
    "Identifier": "xyzzy"
  },
  {
    "Throw": {
      "New": {
        "ArgumentList": [
          {
            "Literal": "\"Nothing happens.\""
          }
        ],
        "Callee": {
          "Identifier": "TypeError"
        }
      }
    }
  }
]
	`)

		// When run, this will call a type error to be thrown
		// This is essentially the same as:
		//
		// var abc = 1(function(){})()
		//
		test(`
        var abc = 1
        (function(){
        })()
        ---
[
   {
      "Var": [
         {
            "Binding": {
               "Initializer": {
                  "Call": {
                     "ArgumentList": [],
                     "Callee": {
                        "Call": {
                           "ArgumentList": [
                              {
                                 "Function": {
                                    "BlockStatement": []
                                 }
                              }
                           ],
                           "Callee": {
                              "Literal": 1
                           }
                        }
                     }
                  }
               },
               "Target": {
                  "Identifier": "abc"
               }
            }
         }
      ]
   }
]
        `)

		test(`
        "use strict"
        ---
[
  {
    "Literal": "\"use strict\""
  }
]
        `)

		test(`
        "use strict"
        abc = 1 + 2 + 11
        ---
[
  {
    "Literal": "\"use strict\""
  },
  {
    "Assign": {
      "Left": {
        "Identifier": "abc"
      },
      "Right": {
        "BinaryExpression": {
          "Left": {
            "BinaryExpression": {
              "Left": {
                "Literal": 1
              },
              "Operator": "+",
              "Right": {
                "Literal": 2
              }
            }
          },
          "Operator": "+",
          "Right": {
            "Literal": 11
          }
        }
      }
    }
  }
]
        `)

		test(`
        abc = function() { 'use strict' }
        ---
[
  {
    "Assign": {
      "Left": {
        "Identifier": "abc"
      },
      "Right": {
        "Function": {
          "BlockStatement": [
            {
              "Literal": "'use strict'"
            }
          ]
        }
      }
    }
  }
]
        `)

		test(`
        for (var abc in def) {
        }
        ---
[
   {
	  "ForIn": {
		 "Body": {
			"BlockStatement": []
		 },
		 "Into": {
			"For-Into-Var": {
			   "Binding": {
				  "Initializer": null,
				  "Target": {
					 "Identifier": "abc"
				  }
			   }
			}
		 },
		 "Source": {
			"Identifier": "def"
		 }
	  }
   }
]
        `)

		test(`
        abc = {
            '"': "'",
            "'": '"',
        }
        ---
[
  {
    "Assign": {
      "Left": {
        "Identifier": "abc"
      },
      "Right": {
        "Object": [
          {
			"Key": {
			   "Idx": 21,
               "Literal": "'\"'",
               "Value": "\""
            },
            "Value": {
              "Literal": "\"'\""
            }
          },
          {
			"Key": {
			   "Idx": 43,
			   "Literal": "\"'\"",
			   "Value": "'"
			},
            "Value": {
              "Literal": "'\"'"
            }
          }
        ]
      }
    }
  }
]
            `)

	})
}
