import { createApp, nodeOps, render } from '@vue/runtime-test'
import { defineComponent, h, ref } from '../src'

describe('api: expose', () => {
  test('via setup context', () => {
    const Child = defineComponent({
      render() {},
      setup(_, { expose }) {
        expose({
          foo: 1,
          bar: ref(2)
        })
        return {
          bar: ref(3),
          baz: ref(4)
        }
      }
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value).toBeTruthy()
    expect(childRef.value.foo).toBe(1)
    expect(childRef.value.bar).toBe(2)
    expect(childRef.value.baz).toBeUndefined()
  })

  test('via options', () => {
    const Child = defineComponent({
      render() {},
      data() {
        return {
          foo: 1
        }
      },
      setup() {
        return {
          bar: ref(2),
          baz: ref(3)
        }
      },
      expose: ['foo', 'bar']
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value).toBeTruthy()
    expect(childRef.value.foo).toBe(1)
    expect(childRef.value.bar).toBe(2)
    expect(childRef.value.baz).toBeUndefined()
  })

  test('options + context', () => {
    const Child = defineComponent({
      render() {},
      expose: ['foo'],
      data() {
        return {
          foo: 1
        }
      },
      setup(_, { expose }) {
        expose({
          bar: ref(2)
        })
        return {
          bar: ref(3),
          baz: ref(4)
        }
      }
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value).toBeTruthy()
    expect(childRef.value.foo).toBe(1)
    expect(childRef.value.bar).toBe(2)
    expect(childRef.value.baz).toBeUndefined()
  })

  test('options: empty', () => {
    const Child = defineComponent({
      render() {},
      expose: [],
      data() {
        return {
          foo: 1
        }
      }
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value).toBeTruthy()
    expect('foo' in childRef.value).toBe(false)
  })

  test('options: empty + setup context', () => {
    const Child = defineComponent({
      render() {},
      expose: [],
      setup(_, { expose }) {
        expose({
          foo: 1
        })
      }
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value).toBeTruthy()
    expect(childRef.value.foo).toBe(1)
  })

  test('with $parent/$root', () => {
    const Child = defineComponent({
      render() {
        expect((this.$parent! as any).foo).toBe(1)
        expect((this.$parent! as any).bar).toBe(undefined)
        expect((this.$root! as any).foo).toBe(1)
        expect((this.$root! as any).bar).toBe(undefined)
      }
    })

    const Parent = defineComponent({
      expose: [],
      setup(_, { expose }) {
        expose({
          foo: 1
        })
        return {
          bar: 2
        }
      },
      render() {
        return h(Child)
      }
    })
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
  })

  test('with mount', () => {
    const Component = defineComponent({
      setup(_, { expose }) {
        expose({
          foo: 1
        })
        return {
          bar: 2
        }
      },
      render() {
        return h('div')
      }
    })
    const root = nodeOps.createElement('div')
    const vm = createApp(Component).mount(root) as any
    expect(vm.foo).toBe(1)
    expect(vm.bar).toBe(undefined)
  })

  test('expose should allow access to built-in instance properties', () => {
    const GrandChild = defineComponent({
      render() {
        return h('div')
      }
    })

    const grandChildRef = ref()
    const Child = defineComponent({
      render() {
        return h('div')
      },
      setup(_, { expose }) {
        expose()
        return () => h(GrandChild, { ref: grandChildRef })
      }
    })

    const childRef = ref()
    const Parent = {
      setup() {
        return () => h(Child, { ref: childRef })
      }
    }
    const root = nodeOps.createElement('div')
    render(h(Parent), root)
    expect(childRef.value.$el.tag).toBe('div')
    expect(grandChildRef.value.$parent).toBe(childRef.value)
    expect(grandChildRef.value.$parent.$parent).toBe(grandChildRef.value.$root)
  })
})
