import {
  ConstantTypes,
  type DirectiveArguments,
  type ForCodegenNode,
  type IfConditionalExpression,
  NodeTypes,
  type RootNode,
  type VNodeCall,
  createArrayExpression,
  createAssignmentExpression,
  createBlockStatement,
  createCacheExpression,
  createCallExpression,
  createCompoundExpression,
  createConditionalExpression,
  createIfStatement,
  createInterpolation,
  createObjectExpression,
  createObjectProperty,
  createSimpleExpression,
  createTemplateLiteral,
  createVNodeCall,
  generate,
  locStub,
} from '../src'
import {
  CREATE_COMMENT,
  CREATE_ELEMENT_VNODE,
  CREATE_VNODE,
  FRAGMENT,
  RENDER_LIST,
  RESOLVE_COMPONENT,
  RESOLVE_DIRECTIVE,
  TO_DISPLAY_STRING,
  helperNameMap,
} from '../src/runtimeHelpers'
import { createElementWithCodegen, genFlagText } from './testUtils'
import { PatchFlags } from '@vue/shared'

function createRoot(options: Partial<RootNode> = {}): RootNode {
  return {
    type: NodeTypes.ROOT,
    source: '',
    children: [],
    helpers: new Set(),
    components: [],
    directives: [],
    imports: [],
    hoists: [],
    cached: [],
    temps: 0,
    codegenNode: createSimpleExpression(`null`, false),
    loc: locStub,
    ...options,
  }
}

describe('compiler: codegen', () => {
  test('module mode preamble', () => {
    const root = createRoot({
      helpers: new Set([CREATE_VNODE, RESOLVE_DIRECTIVE]),
    })
    const { code } = generate(root, { mode: 'module' })
    expect(code).toMatch(
      `import { ${helperNameMap[CREATE_VNODE]} as _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${helperNameMap[RESOLVE_DIRECTIVE]} } from "vue"`,
    )
    expect(code).toMatchSnapshot()
  })

  test('module mode preamble w/ optimizeImports: true', () => {
    const root = createRoot({
      helpers: new Set([CREATE_VNODE, RESOLVE_DIRECTIVE]),
    })
    const { code } = generate(root, { mode: 'module', optimizeImports: true })
    expect(code).toMatch(
      `import { ${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]} } from "vue"`,
    )
    expect(code).toMatch(
      `const _${helperNameMap[CREATE_VNODE]} = ${helperNameMap[CREATE_VNODE]}, _${helperNameMap[RESOLVE_DIRECTIVE]} = ${helperNameMap[RESOLVE_DIRECTIVE]}`,
    )
    expect(code).toMatchSnapshot()
  })

  test('function mode preamble', () => {
    const root = createRoot({
      helpers: new Set([CREATE_VNODE, RESOLVE_DIRECTIVE]),
    })
    const { code } = generate(root, { mode: 'function' })
    expect(code).toMatch(`const _Vue = Vue`)
    expect(code).toMatch(
      `const { ${helperNameMap[CREATE_VNODE]}: _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]}: _${helperNameMap[RESOLVE_DIRECTIVE]} } = _Vue`,
    )
    expect(code).toMatchSnapshot()
  })

  test('function mode preamble w/ prefixIdentifiers: true', () => {
    const root = createRoot({
      helpers: new Set([CREATE_VNODE, RESOLVE_DIRECTIVE]),
    })
    const { code } = generate(root, {
      mode: 'function',
      prefixIdentifiers: true,
    })
    expect(code).not.toMatch(`const _Vue = Vue`)
    expect(code).toMatch(
      `const { ${helperNameMap[CREATE_VNODE]}: _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]}: _${helperNameMap[RESOLVE_DIRECTIVE]} } = Vue`,
    )
    expect(code).toMatchSnapshot()
  })

  test('assets + temps', () => {
    const root = createRoot({
      components: [`Foo`, `bar-baz`, `barbaz`, `Qux__self`],
      directives: [`my_dir_0`, `my_dir_1`],
      temps: 3,
    })
    const { code } = generate(root, { mode: 'function' })
    expect(code).toMatch(
      `const _component_Foo = _${helperNameMap[RESOLVE_COMPONENT]}("Foo")\n`,
    )
    expect(code).toMatch(
      `const _component_bar_baz = _${helperNameMap[RESOLVE_COMPONENT]}("bar-baz")\n`,
    )
    expect(code).toMatch(
      `const _component_barbaz = _${helperNameMap[RESOLVE_COMPONENT]}("barbaz")\n`,
    )
    // implicit self reference from SFC filename
    expect(code).toMatch(
      `const _component_Qux = _${helperNameMap[RESOLVE_COMPONENT]}("Qux", true)\n`,
    )
    expect(code).toMatch(
      `const _directive_my_dir_0 = _${helperNameMap[RESOLVE_DIRECTIVE]}("my_dir_0")\n`,
    )
    expect(code).toMatch(
      `const _directive_my_dir_1 = _${helperNameMap[RESOLVE_DIRECTIVE]}("my_dir_1")\n`,
    )
    expect(code).toMatch(`let _temp0, _temp1, _temp2`)
    expect(code).toMatchSnapshot()
  })

  test('hoists', () => {
    const root = createRoot({
      hoists: [
        createSimpleExpression(`hello`, false, locStub),
        createObjectExpression(
          [
            createObjectProperty(
              createSimpleExpression(`id`, true, locStub),
              createSimpleExpression(`foo`, true, locStub),
            ),
          ],
          locStub,
        ),
      ],
    })
    const { code } = generate(root)
    expect(code).toMatch(`const _hoisted_1 = hello`)
    expect(code).toMatch(`const _hoisted_2 = { id: "foo" }`)
    expect(code).toMatchSnapshot()
  })

  test('temps', () => {
    const root = createRoot({
      temps: 3,
    })
    const { code } = generate(root)
    expect(code).toMatch(`let _temp0, _temp1, _temp2`)
    expect(code).toMatchSnapshot()
  })

  test('static text', () => {
    const { code } = generate(
      createRoot({
        codegenNode: {
          type: NodeTypes.TEXT,
          content: 'hello',
          loc: locStub,
        },
      }),
    )
    expect(code).toMatch(`return "hello"`)
    expect(code).toMatchSnapshot()
  })

  test('interpolation', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createInterpolation(`hello`, locStub),
      }),
    )
    expect(code).toMatch(`return _${helperNameMap[TO_DISPLAY_STRING]}(hello)`)
    expect(code).toMatchSnapshot()
  })

  test('comment', () => {
    const { code } = generate(
      createRoot({
        codegenNode: {
          type: NodeTypes.COMMENT,
          content: 'foo',
          loc: locStub,
        },
      }),
    )
    expect(code).toMatch(`return _${helperNameMap[CREATE_COMMENT]}("foo")`)
    expect(code).toMatchSnapshot()
  })

  test('compound expression', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createCompoundExpression([
          `_ctx.`,
          createSimpleExpression(`foo`, false, locStub),
          ` + `,
          {
            type: NodeTypes.INTERPOLATION,
            loc: locStub,
            content: createSimpleExpression(`bar`, false, locStub),
          },
          // nested compound
          createCompoundExpression([` + `, `nested`]),
        ]),
      }),
    )
    expect(code).toMatch(
      `return _ctx.foo + _${helperNameMap[TO_DISPLAY_STRING]}(bar) + nested`,
    )
    expect(code).toMatchSnapshot()
  })

  test('ifNode', () => {
    const { code } = generate(
      createRoot({
        codegenNode: {
          type: NodeTypes.IF,
          loc: locStub,
          branches: [],
          codegenNode: createConditionalExpression(
            createSimpleExpression('foo', false),
            createSimpleExpression('bar', false),
            createSimpleExpression('baz', false),
          ) as IfConditionalExpression,
        },
      }),
    )
    expect(code).toMatch(/return foo\s+\? bar\s+: baz/)
    expect(code).toMatchSnapshot()
  })

  test('forNode', () => {
    const { code } = generate(
      createRoot({
        codegenNode: {
          type: NodeTypes.FOR,
          loc: locStub,
          source: createSimpleExpression('foo', false),
          valueAlias: undefined,
          keyAlias: undefined,
          objectIndexAlias: undefined,
          children: [],
          parseResult: {} as any,
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: FRAGMENT,
            isBlock: true,
            disableTracking: true,
            props: undefined,
            children: createCallExpression(RENDER_LIST),
            patchFlag: PatchFlags.TEXT,
            dynamicProps: undefined,
            directives: undefined,
            loc: locStub,
          } as ForCodegenNode,
        },
      }),
    )
    expect(code).toMatch(`openBlock(true)`)
    expect(code).toMatchSnapshot()
  })

  test('forNode with constant expression', () => {
    const { code } = generate(
      createRoot({
        codegenNode: {
          type: NodeTypes.FOR,
          loc: locStub,
          source: createSimpleExpression(
            '1 + 2',
            false,
            locStub,
            ConstantTypes.CAN_STRINGIFY,
          ),
          valueAlias: undefined,
          keyAlias: undefined,
          objectIndexAlias: undefined,
          children: [],
          parseResult: {} as any,
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: FRAGMENT,
            isBlock: true,
            disableTracking: false,
            props: undefined,
            children: createCallExpression(RENDER_LIST),
            patchFlag: PatchFlags.STABLE_FRAGMENT,
            dynamicProps: undefined,
            directives: undefined,
            loc: locStub,
          } as ForCodegenNode,
        },
      }),
    )
    expect(code).toMatch(`openBlock()`)
    expect(code).toMatchSnapshot()
  })

  test('Element (callExpression + objectExpression + TemplateChildNode[])', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createElementWithCodegen(
          // string
          `"div"`,
          // ObjectExpression
          createObjectExpression(
            [
              createObjectProperty(
                createSimpleExpression(`id`, true, locStub),
                createSimpleExpression(`foo`, true, locStub),
              ),
              createObjectProperty(
                createSimpleExpression(`prop`, false, locStub),
                createSimpleExpression(`bar`, false, locStub),
              ),
              // compound expression as computed key
              createObjectProperty(
                {
                  type: NodeTypes.COMPOUND_EXPRESSION,
                  loc: locStub,
                  children: [
                    `foo + `,
                    createSimpleExpression(`bar`, false, locStub),
                  ],
                },
                createSimpleExpression(`bar`, false, locStub),
              ),
            ],
            locStub,
          ),
          // ChildNode[]
          [
            createElementWithCodegen(
              `"p"`,
              createObjectExpression(
                [
                  createObjectProperty(
                    // should quote the key!
                    createSimpleExpression(`some-key`, true, locStub),
                    createSimpleExpression(`foo`, true, locStub),
                  ),
                ],
                locStub,
              ),
            ),
          ],
          // flag
          PatchFlags.FULL_PROPS,
        ),
      }),
    )
    expect(code).toMatch(`
    return _${helperNameMap[CREATE_ELEMENT_VNODE]}("div", {
      id: "foo",
      [prop]: bar,
      [foo + bar]: bar
    }, [
      _${helperNameMap[CREATE_ELEMENT_VNODE]}("p", { "some-key": "foo" })
    ], ${genFlagText(PatchFlags.FULL_PROPS)})`)
    expect(code).toMatchSnapshot()
  })

  test('ArrayExpression', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createArrayExpression([
          createSimpleExpression(`foo`, false),
          createCallExpression(`bar`, [`baz`]),
        ]),
      }),
    )
    expect(code).toMatch(`return [
      foo,
      bar(baz)
    ]`)
    expect(code).toMatchSnapshot()
  })

  test('ConditionalExpression', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createConditionalExpression(
          createSimpleExpression(`ok`, false),
          createCallExpression(`foo`),
          createConditionalExpression(
            createSimpleExpression(`orNot`, false),
            createCallExpression(`bar`),
            createCallExpression(`baz`),
          ),
        ),
      }),
    )
    expect(code).toMatch(
      `return ok
      ? foo()
      : orNot
        ? bar()
        : baz()`,
    )
    expect(code).toMatchSnapshot()
  })

  test('CacheExpression', () => {
    const { code } = generate(
      createRoot({
        cached: [],
        codegenNode: createCacheExpression(
          1,
          createSimpleExpression(`foo`, false),
        ),
      }),
      {
        mode: 'module',
        prefixIdentifiers: true,
      },
    )
    expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`)
    expect(code).toMatchSnapshot()
  })

  test('CacheExpression w/ isVOnce: true', () => {
    const { code } = generate(
      createRoot({
        cached: [],
        codegenNode: createCacheExpression(
          1,
          createSimpleExpression(`foo`, false),
          true,
        ),
      }),
      {
        mode: 'module',
        prefixIdentifiers: true,
      },
    )
    expect(code).toMatch(
      `
  _cache[1] || (
    _setBlockTracking(-1),
    (_cache[1] = foo).cacheIndex = 1,
    _setBlockTracking(1),
    _cache[1]
  )
    `.trim(),
    )
    expect(code).toMatchSnapshot()
  })

  test('TemplateLiteral', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createCallExpression(`_push`, [
          createTemplateLiteral([
            `foo`,
            createCallExpression(`_renderAttr`, ['id', 'foo']),
            `bar`,
          ]),
        ]),
      }),
      { ssr: true, mode: 'module' },
    )
    expect(code).toMatchInlineSnapshot(`
      "
      export function ssrRender(_ctx, _push, _parent, _attrs) {
        _push(\`foo\${_renderAttr(id, foo)}bar\`)
      }"
    `)
  })

  describe('IfStatement', () => {
    test('if', () => {
      const { code } = generate(
        createRoot({
          codegenNode: createBlockStatement([
            createIfStatement(
              createSimpleExpression('foo', false),
              createBlockStatement([createCallExpression(`ok`)]),
            ),
          ]),
        }),
        { ssr: true, mode: 'module' },
      )
      expect(code).toMatchInlineSnapshot(`
        "
        export function ssrRender(_ctx, _push, _parent, _attrs) {
          if (foo) {
            ok()
          }
        }"
      `)
    })

    test('if/else', () => {
      const { code } = generate(
        createRoot({
          codegenNode: createBlockStatement([
            createIfStatement(
              createSimpleExpression('foo', false),
              createBlockStatement([createCallExpression(`foo`)]),
              createBlockStatement([createCallExpression('bar')]),
            ),
          ]),
        }),
        { ssr: true, mode: 'module' },
      )
      expect(code).toMatchInlineSnapshot(`
        "
        export function ssrRender(_ctx, _push, _parent, _attrs) {
          if (foo) {
            foo()
          } else {
            bar()
          }
        }"
      `)
    })

    test('if/else-if', () => {
      const { code } = generate(
        createRoot({
          codegenNode: createBlockStatement([
            createIfStatement(
              createSimpleExpression('foo', false),
              createBlockStatement([createCallExpression(`foo`)]),
              createIfStatement(
                createSimpleExpression('bar', false),
                createBlockStatement([createCallExpression(`bar`)]),
              ),
            ),
          ]),
        }),
        { ssr: true, mode: 'module' },
      )
      expect(code).toMatchInlineSnapshot(`
        "
        export function ssrRender(_ctx, _push, _parent, _attrs) {
          if (foo) {
            foo()
          } else if (bar) {
            bar()
          }
        }"
      `)
    })

    test('if/else-if/else', () => {
      const { code } = generate(
        createRoot({
          codegenNode: createBlockStatement([
            createIfStatement(
              createSimpleExpression('foo', false),
              createBlockStatement([createCallExpression(`foo`)]),
              createIfStatement(
                createSimpleExpression('bar', false),
                createBlockStatement([createCallExpression(`bar`)]),
                createBlockStatement([createCallExpression('baz')]),
              ),
            ),
          ]),
        }),
        { ssr: true, mode: 'module' },
      )
      expect(code).toMatchInlineSnapshot(`
        "
        export function ssrRender(_ctx, _push, _parent, _attrs) {
          if (foo) {
            foo()
          } else if (bar) {
            bar()
          } else {
            baz()
          }
        }"
      `)
    })
  })

  test('AssignmentExpression', () => {
    const { code } = generate(
      createRoot({
        codegenNode: createAssignmentExpression(
          createSimpleExpression(`foo`, false),
          createSimpleExpression(`bar`, false),
        ),
      }),
    )
    expect(code).toMatchInlineSnapshot(`
      "
      return function render(_ctx, _cache) {
        with (_ctx) {
          return foo = bar
        }
      }"
    `)
  })

  describe('VNodeCall', () => {
    function genCode(node: VNodeCall) {
      return generate(
        createRoot({
          codegenNode: node,
        }),
      ).code.match(/with \(_ctx\) \{\s+([^]+)\s+\}\s+\}$/)![1]
    }

    const mockProps = createObjectExpression([
      createObjectProperty(`foo`, createSimpleExpression(`bar`, true)),
    ])
    const mockChildren = createCompoundExpression(['children'])
    const mockDirs = createArrayExpression([
      createArrayExpression([`foo`, createSimpleExpression(`bar`, false)]),
    ]) as DirectiveArguments

    test('tag only', () => {
      expect(genCode(createVNodeCall(null, `"div"`))).toMatchInlineSnapshot(`
        "return _createElementVNode("div")
         "
      `)
      expect(genCode(createVNodeCall(null, FRAGMENT))).toMatchInlineSnapshot(`
              "return _createElementVNode(_Fragment)
               "
          `)
    })

    test('with props', () => {
      expect(genCode(createVNodeCall(null, `"div"`, mockProps)))
        .toMatchInlineSnapshot(`
          "return _createElementVNode("div", { foo: "bar" })
           "
        `)
    })

    test('with children, no props', () => {
      expect(genCode(createVNodeCall(null, `"div"`, undefined, mockChildren)))
        .toMatchInlineSnapshot(`
          "return _createElementVNode("div", null, children)
           "
        `)
    })

    test('with children + props', () => {
      expect(genCode(createVNodeCall(null, `"div"`, mockProps, mockChildren)))
        .toMatchInlineSnapshot(`
          "return _createElementVNode("div", { foo: "bar" }, children)
           "
        `)
    })

    test('with patchFlag and no children/props', () => {
      expect(
        genCode(
          createVNodeCall(null, `"div"`, undefined, undefined, PatchFlags.TEXT),
        ),
      ).toMatchInlineSnapshot(`
        "return _createElementVNode("div", null, null, 1 /* TEXT */)
         "
      `)
    })

    test('as block', () => {
      expect(
        genCode(
          createVNodeCall(
            null,
            `"div"`,
            mockProps,
            mockChildren,
            undefined,
            undefined,
            undefined,
            true,
          ),
        ),
      ).toMatchInlineSnapshot(`
        "return (_openBlock(), _createElementBlock("div", { foo: "bar" }, children))
         "
      `)
    })

    test('as for block', () => {
      expect(
        genCode(
          createVNodeCall(
            null,
            `"div"`,
            mockProps,
            mockChildren,
            undefined,
            undefined,
            undefined,
            true,
            true,
          ),
        ),
      ).toMatchInlineSnapshot(`
        "return (_openBlock(true), _createElementBlock("div", { foo: "bar" }, children))
         "
      `)
    })

    test('with directives', () => {
      expect(
        genCode(
          createVNodeCall(
            null,
            `"div"`,
            mockProps,
            mockChildren,
            undefined,
            undefined,
            mockDirs,
          ),
        ),
      ).toMatchInlineSnapshot(`
        "return _withDirectives(_createElementVNode("div", { foo: "bar" }, children), [
              [foo, bar]
            ])
         "
      `)
    })

    test('block + directives', () => {
      expect(
        genCode(
          createVNodeCall(
            null,
            `"div"`,
            mockProps,
            mockChildren,
            undefined,
            undefined,
            mockDirs,
            true,
          ),
        ),
      ).toMatchInlineSnapshot(`
        "return _withDirectives((_openBlock(), _createElementBlock("div", { foo: "bar" }, children)), [
              [foo, bar]
            ])
         "
      `)
    })
  })
})
