"use strict"

const assert = require("assert")
const acorn = require("acorn")
const Parser = acorn.Parser.extend(require(".."))

function test(text, expectedResult, additionalOptions) {
  it(text, function () {
    const result = Parser.parse(text, Object.assign({ ecmaVersion: 10 }, additionalOptions))
    if (expectedResult) {
      assert.deepStrictEqual(result.body[0], expectedResult)
    }
  })
}
function testFail(text, expectedResult, additionalOptions) {
  it(text, function () {
    let failed = false
    try {
      Parser.parse(text, Object.assign({ ecmaVersion: 10 }, additionalOptions))
    } catch (e) {
      assert.strictEqual(e.message, expectedResult)
      failed = true
    }
    assert(failed)
  })
}
const newNode = (start, props) => Object.assign(new acorn.Node({options: {}}, start), props)

describe("acorn-static-class-features", function () {
  test(`class CustomDate {
    // ...
    static epoch = new CustomDate(0);
  }`)

  testFail("class A { static #a; f() { delete A.#a } }", "Private elements may not be deleted (1:27)")
  testFail("class A { static #a; static #a }", "Duplicate private element (1:21)")
  testFail("class A { static a = A.#a }", "Usage of undeclared private name (1:23)")
  testFail("class A { static a = () => arguments }", "A static class field initializer may not contain arguments (1:27)")
  testFail("class A { static a = () => super() }", "'super' keyword outside a method (1:27)")
  testFail("class A { static # a }", "Unexpected token (1:19)")
  testFail("class A { static #a; a() { A.# a } }", "Unexpected token (1:31)")
  test(`class C {
    static async * #method() {
    }
  }`)

  const classes = [
    { text: "class A { %s }", ast: getBody => {
      const body = getBody(10)
      return newNode(0, {
        type: "ClassDeclaration",
        end: body.end + 2,
        id: newNode(6, {
          type: "Identifier",
          end: 7,
          name: "A"
        }),
        superClass: null,
        body: newNode(8, {
          type: "ClassBody",
          end: body.end + 2,
          body: [body]
        })
      })
    } },
    { text: "class A { %s; }", ast: getBody => {
      const body = getBody(10)
      return newNode(0, {
        type: "ClassDeclaration",
        end: body.end + 3,
        id: newNode(6, {
          type: "Identifier",
          end: 7,
          name: "A"
        }),
        superClass: null,
        body: newNode(8, {
          type: "ClassBody",
          end: body.end + 3,
          body: [body]
        })
      })
    } },
    { text: "class A { %s; static #y }", ast: getBody => {
      const body = getBody(10)
      return newNode(0, {
        type: "ClassDeclaration",
        end: body.end + 13,
        id: newNode(6, {
          type: "Identifier",
          end: 7,
          name: "A"
        }),
        superClass: null,
        body: newNode(8, {
          type: "ClassBody",
          end: body.end + 13,
          body: [body, newNode(body.end + 2, {
            type: "FieldDefinition",
            end: body.end + 11,
            key: newNode(body.end + 9, {
              type: "PrivateName",
              end: body.end + 11,
              name: "y"
            }),
            value: null,
            computed: false,
            static: true
          }) ]
        })
      })
    } },
    { text: "class A { %s;a() {} }", ast: getBody => {
      const body = getBody(10)
      return newNode(0, {
        type: "ClassDeclaration",
        end: body.end + 9,
        id: newNode(6, {
          type: "Identifier",
          end: 7,
          name: "A"
        }),
        superClass: null,
        body: newNode(8, {
          type: "ClassBody",
          end: body.end + 9,
          body: [ body, newNode(body.end + 1, {
            type: "MethodDefinition",
            end: body.end + 7,
            kind: "method",
            static: false,
            computed: false,
            key: newNode(body.end + 1, {
              type: "Identifier",
              end: body.end + 2,
              name: "a"
            }),
            value: newNode(body.end + 2, {
              type: "FunctionExpression",
              end: body.end + 7,
              id: null,
              generator: false,
              expression: false,
              async: false,
              params: [],
              body: newNode(body.end + 5, {
                type: "BlockStatement",
                end: body.end + 7,
                body: []
              })
            })
          }) ]
        })
      })
    } },
    { text: "class A { %s\na() {} }", ast: getBody => {
      const body = getBody(10)
      return newNode(0, {
        type: "ClassDeclaration",
        end: body.end + 9,
        id: newNode(6, {
          type: "Identifier",
          end: 7,
          name: "A"
        }),
        superClass: null,
        body: newNode(8, {
          type: "ClassBody",
          end: body.end + 9,
          body: [
            body,
            newNode(body.end + 1, {
              type: "MethodDefinition",
              end: body.end + 7,
              kind: "method",
              static: false,
              computed: false,
              key: newNode(body.end + 1, {
                type: "Identifier",
                end: body.end + 2,
                name: "a"
              }),
              value: newNode(body.end + 2, {
                type: "FunctionExpression",
                end: body.end + 7,
                id: null,
                generator: false,
                expression: false,
                async: false,
                params: [],
                body: newNode(body.end + 5, {
                  type: "BlockStatement",
                  end: body.end + 7,
                  body: []
                })
              })
            })
          ]
        })
      })
    } },
  ]

  ;[
    { body: "static x", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 8,
      key: newNode(start + 7, {
        type: "Identifier",
        end: start + 8,
        name: "x"
      }),
      value: null,
      computed: false,
      static: true
    }) },
    { body: "static x = 0", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 12,
      key: newNode(start + 7, {
        type: "Identifier",
        end: start + 8,
        name: "x"
      }),
      value: newNode(start + 11, {
        type: "Literal",
        end: start + 12,
        value: 0,
        raw: "0"
      }),
      computed: false,
      static: true
    }) },
    { body: "static [x]", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 10,
      computed: true,
      key: newNode(start + 8, {
        type: "Identifier",
        end: start + 9,
        name: "x"
      }),
      value: null,
      static: true
    }) },
    { body: "static [x] = 0", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 14,
      computed: true,
      key: newNode(start + 8, {
        type: "Identifier",
        end: start + 9,
        name: "x"
      }),
      value: newNode(start + 13, {
        type: "Literal",
        end: start + 14,
        value: 0,
        raw: "0"
      }),
      static: true
    }) },
    { body: "static #x", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 9,
      computed: false,
      key: newNode(start + 7, {
        type: "PrivateName",
        end: start + 9,
        name: "x"
      }),
      value: null,
      static: true
    }) },
    { body: "static #x = 0", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 13,
      computed: false,
      key: newNode(start + 7, {
        type: "PrivateName",
        end: start + 9,
        name: "x"
      }),
      value: newNode(start + 12, {
        type: "Literal",
        end: start + 13,
        value: 0,
        raw: "0"
      }),
      static: true
    }) },

    { body: "static async", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 12,
      key: newNode(start + 7, {
        type: "Identifier",
        end: start + 12,
        name: "async"
      }),
      value: null,
      computed: false,
      static: true
    }) },

    { body: "static async = 5", passes: true, ast: start => newNode(start, {
      type: "FieldDefinition",
      end: start + 16,
      key: newNode(start + 7, {
        type: "Identifier",
        end: start + 12,
        name: "async"
      }),
      value: newNode(start + 15, {
        type: "Literal",
        end: start + 16,
        raw: "5",
        value: 5
      }),
      computed: false,
      static: true
    }) },
  ].forEach(bodyInput => {
    const body = bodyInput.body, passes = bodyInput.passes, bodyAst = bodyInput.ast
    classes.forEach(input => {
      const text = input.text, options = input.options || {}, ast = input.ast
      ;(passes ? test : testFail)(text.replace("%s", body), ast(bodyAst), options)
    })
  })

  // Private static methods
  test("class A { a() { A.#a }; static #a() {} }")

  testFail("class A { static #a() {}; f() { delete A.#a } }", "Private elements may not be deleted (1:32)")
  testFail("class A { static #a() {}; static #a() {} }", "Duplicate private element (1:26)")
  test("class A { static get #a() {}; static set #a(newA) {} }")
  testFail("class A { a() { A.#a } }", "Usage of undeclared private name (1:18)")
  testFail("class A { a() { A.#a } b() { A.#b } }", "Usage of undeclared private name (1:18)")
  testFail("class A { static #constructor() {} }", "Classes may not have a private element named constructor (1:17)")
  testFail("class A { static #[ab]() {} }", "Unexpected token (1:18)")
  testFail("a = { static #ab() {} }", "Unexpected token (1:13)")
  testFail("class A { static [{#ab() {}}]() {} }", "Unexpected token (1:19)")
  testFail("class A{ static # a() {}}", "Unexpected token (1:18)")
  testFail("class C{ static #method() { super(); } };", "super() call outside constructor of a subclass (1:28)")
  test("class C{ static #method() { super.y(); } };")

  ;[
    { body: "static #x() {}", passes: true, ast: start => newNode(start, {
      type: "MethodDefinition",
      end: start + 14,
      computed: false,
      key: newNode(start + 7, {
        type: "PrivateName",
        end: start + 9,
        name: "x"
      }),
      kind: "method",
      static: true,
      value: newNode(start + 9, {
        type: "FunctionExpression",
        end: start + 14,
        async: false,
        body: newNode(start + 12, {
          type: "BlockStatement",
          body: [],
          end: start + 14,
        }),
        expression: false,
        generator: false,
        id: null,
        params: [],
      })
    }) },
    { body: "static get #x() {}", passes: true, ast: start => newNode(start, {
      type: "MethodDefinition",
      end: start + 18,
      computed: false,
      key: newNode(start + 11, {
        type: "PrivateName",
        end: start + 13,
        name: "x"
      }),
      kind: "get",
      static: true,
      value: newNode(start + 13, {
        type: "FunctionExpression",
        end: start + 18,
        async: false,
        body: newNode(start + 16, {
          body: [],
          end: start + 18,
          type: "BlockStatement"
        }),
        expression: false,
        generator: false,
        id: null,
        params: [],
      })
    }) },

  ].forEach(bodyInput => {
    const body = bodyInput.body, passes = bodyInput.passes, bodyAst = bodyInput.ast
    classes.forEach(input => {
      const text = input.text, options = input.options || {}, ast = input.ast
      ;(passes ? test : testFail)(text.replace("%s", body), ast(bodyAst), options)
    })
  })
  test("class A extends B { constructor() { super() } }")
})
