import {
  describe,
  Component,
  defineComponent,
  PropType,
  ref,
  Ref,
  expectError,
  expectType,
  ShallowUnwrapRef,
  FunctionalComponent,
  ComponentPublicInstance,
  toRefs
} from './index'

declare function extractComponentOptions<Props, RawBindings>(
  obj: Component<Props, RawBindings>
): {
  props: Props
  rawBindings: RawBindings
  setup: ShallowUnwrapRef<RawBindings>
}

describe('object props', () => {
  interface ExpectedProps {
    a?: number | undefined
    b: string
    e?: Function
    bb: string
    bbb: string
    cc?: string[] | undefined
    dd: { n: 1 }
    ee?: () => string
    ff?: (a: number, b: string) => { a: boolean }
    ccc?: string[] | undefined
    ddd: string[]
    eee: () => { a: string }
    fff: (a: number, b: string) => { a: boolean }
    hhh: boolean
    ggg: 'foo' | 'bar'
    ffff: (a: number, b: string) => { a: boolean }
    validated?: string
    object?: object
  }

  interface ExpectedRefs {
    a: Ref<number | undefined>
    b: Ref<string>
    e: Ref<Function | undefined>
    bb: Ref<string>
    bbb: Ref<string>
    cc: Ref<string[] | undefined>
    dd: Ref<{ n: 1 }>
    ee: Ref<(() => string) | undefined>
    ff: Ref<((a: number, b: string) => { a: boolean }) | undefined>
    ccc: Ref<string[] | undefined>
    ddd: Ref<string[]>
    eee: Ref<() => { a: string }>
    fff: Ref<(a: number, b: string) => { a: boolean }>
    hhh: Ref<boolean>
    ggg: Ref<'foo' | 'bar'>
    ffff: Ref<(a: number, b: string) => { a: boolean }>
    validated: Ref<string | undefined>
    object: Ref<object | undefined>
  }

  describe('defineComponent', () => {
    const MyComponent = defineComponent({
      props: {
        a: Number,
        // required should make property non-void
        b: {
          type: String,
          required: true
        },
        e: Function,
        // default value should infer type and make it non-void
        bb: {
          default: 'hello'
        },
        bbb: {
          // Note: default function value requires arrow syntax + explicit
          // annotation
          default: (props: any) => (props.bb as string) || 'foo'
        },
        // explicit type casting
        cc: Array as PropType<string[]>,
        // required + type casting
        dd: {
          type: Object as PropType<{ n: 1 }>,
          required: true
        },
        // return type
        ee: Function as PropType<() => string>,
        // arguments + object return
        ff: Function as PropType<(a: number, b: string) => { a: boolean }>,
        // explicit type casting with constructor
        ccc: Array as () => string[],
        // required + contructor type casting
        ddd: {
          type: Array as () => string[],
          required: true
        },
        // required + object return
        eee: {
          type: Function as PropType<() => { a: string }>,
          required: true
        },
        // required + arguments + object return
        fff: {
          type: Function as PropType<(a: number, b: string) => { a: boolean }>,
          required: true
        },
        hhh: {
          type: Boolean,
          required: true
        },
        // default + type casting
        ggg: {
          type: String as PropType<'foo' | 'bar'>,
          default: 'foo'
        },
        // default + function
        ffff: {
          type: Function as PropType<(a: number, b: string) => { a: boolean }>,
          default: (_a: number, _b: string) => ({ a: true })
        },
        validated: {
          type: String,
          // validator requires explicit annotation
          validator: (val: unknown) => val !== ''
        },
        object: Object as PropType<object>
      },
      setup(props) {
        const refs = toRefs(props)
        expectType<ExpectedRefs['a']>(refs.a)
        expectType<ExpectedRefs['b']>(refs.b)
        expectType<ExpectedRefs['e']>(refs.e)
        expectType<ExpectedRefs['bb']>(refs.bb)
        expectType<ExpectedRefs['bbb']>(refs.bbb)
        expectType<ExpectedRefs['cc']>(refs.cc)
        expectType<ExpectedRefs['dd']>(refs.dd)
        expectType<ExpectedRefs['ee']>(refs.ee)
        expectType<ExpectedRefs['ff']>(refs.ff)
        expectType<ExpectedRefs['ccc']>(refs.ccc)
        expectType<ExpectedRefs['ddd']>(refs.ddd)
        expectType<ExpectedRefs['eee']>(refs.eee)
        expectType<ExpectedRefs['fff']>(refs.fff)
        expectType<ExpectedRefs['hhh']>(refs.hhh)
        expectType<ExpectedRefs['ggg']>(refs.ggg)
        expectType<ExpectedRefs['ffff']>(refs.ffff)
        expectType<ExpectedRefs['validated']>(refs.validated)
        expectType<ExpectedRefs['object']>(refs.object)

        return {
          setupA: 1,
          setupB: ref(1),
          setupC: {
            a: ref(2)
          },
          setupD: undefined as Ref<number> | undefined,
          setupProps: props
        }
      }
    })

    const { props, rawBindings, setup } = extractComponentOptions(MyComponent)

    // props
    expectType<ExpectedProps['a']>(props.a)
    expectType<ExpectedProps['b']>(props.b)
    expectType<ExpectedProps['e']>(props.e)
    expectType<ExpectedProps['bb']>(props.bb)
    expectType<ExpectedProps['bbb']>(props.bbb)
    expectType<ExpectedProps['cc']>(props.cc)
    expectType<ExpectedProps['dd']>(props.dd)
    expectType<ExpectedProps['ee']>(props.ee)
    expectType<ExpectedProps['ff']>(props.ff)
    expectType<ExpectedProps['ccc']>(props.ccc)
    expectType<ExpectedProps['ddd']>(props.ddd)
    expectType<ExpectedProps['eee']>(props.eee)
    expectType<ExpectedProps['fff']>(props.fff)
    expectType<ExpectedProps['hhh']>(props.hhh)
    expectType<ExpectedProps['ggg']>(props.ggg)
    expectType<ExpectedProps['ffff']>(props.ffff)
    expectType<ExpectedProps['validated']>(props.validated)
    expectType<ExpectedProps['object']>(props.object)

    // raw bindings
    expectType<Number>(rawBindings.setupA)
    expectType<Ref<Number>>(rawBindings.setupB)
    expectType<Ref<Number>>(rawBindings.setupC.a)
    expectType<Ref<Number> | undefined>(rawBindings.setupD)

    // raw bindings props
    expectType<ExpectedProps['a']>(rawBindings.setupProps.a)
    expectType<ExpectedProps['b']>(rawBindings.setupProps.b)
    expectType<ExpectedProps['e']>(rawBindings.setupProps.e)
    expectType<ExpectedProps['bb']>(rawBindings.setupProps.bb)
    expectType<ExpectedProps['bbb']>(rawBindings.setupProps.bbb)
    expectType<ExpectedProps['cc']>(rawBindings.setupProps.cc)
    expectType<ExpectedProps['dd']>(rawBindings.setupProps.dd)
    expectType<ExpectedProps['ee']>(rawBindings.setupProps.ee)
    expectType<ExpectedProps['ff']>(rawBindings.setupProps.ff)
    expectType<ExpectedProps['ccc']>(rawBindings.setupProps.ccc)
    expectType<ExpectedProps['ddd']>(rawBindings.setupProps.ddd)
    expectType<ExpectedProps['eee']>(rawBindings.setupProps.eee)
    expectType<ExpectedProps['fff']>(rawBindings.setupProps.fff)
    expectType<ExpectedProps['hhh']>(rawBindings.setupProps.hhh)
    expectType<ExpectedProps['ggg']>(rawBindings.setupProps.ggg)
    expectType<ExpectedProps['ffff']>(rawBindings.setupProps.ffff)
    expectType<ExpectedProps['validated']>(rawBindings.setupProps.validated)

    // setup
    expectType<Number>(setup.setupA)
    expectType<Number>(setup.setupB)
    expectType<Ref<Number>>(setup.setupC.a)
    expectType<number | undefined>(setup.setupD)

    // raw bindings props
    expectType<ExpectedProps['a']>(setup.setupProps.a)
    expectType<ExpectedProps['b']>(setup.setupProps.b)
    expectType<ExpectedProps['e']>(setup.setupProps.e)
    expectType<ExpectedProps['bb']>(setup.setupProps.bb)
    expectType<ExpectedProps['bbb']>(setup.setupProps.bbb)
    expectType<ExpectedProps['cc']>(setup.setupProps.cc)
    expectType<ExpectedProps['dd']>(setup.setupProps.dd)
    expectType<ExpectedProps['ee']>(setup.setupProps.ee)
    expectType<ExpectedProps['ff']>(setup.setupProps.ff)
    expectType<ExpectedProps['ccc']>(setup.setupProps.ccc)
    expectType<ExpectedProps['ddd']>(setup.setupProps.ddd)
    expectType<ExpectedProps['eee']>(setup.setupProps.eee)
    expectType<ExpectedProps['fff']>(setup.setupProps.fff)
    expectType<ExpectedProps['hhh']>(setup.setupProps.hhh)
    expectType<ExpectedProps['ggg']>(setup.setupProps.ggg)
    expectType<ExpectedProps['ffff']>(setup.setupProps.ffff)
    expectType<ExpectedProps['validated']>(setup.setupProps.validated)

    // instance
    const instance = new MyComponent()
    expectType<number>(instance.setupA)
    expectType<number | undefined>(instance.setupD)
    // @ts-expect-error
    instance.notExist
  })

  describe('options', () => {
    const MyComponent = {
      props: {
        a: Number,
        // required should make property non-void
        b: {
          type: String,
          required: true
        },
        e: Function,
        // default value should infer type and make it non-void
        bb: {
          default: 'hello'
        },
        bbb: {
          // Note: default function value requires arrow syntax + explicit
          // annotation
          default: (props: any) => (props.bb as string) || 'foo'
        },
        // explicit type casting
        cc: Array as PropType<string[]>,
        // required + type casting
        dd: {
          type: Object as PropType<{ n: 1 }>,
          required: true
        },
        // return type
        ee: Function as PropType<() => string>,
        // arguments + object return
        ff: Function as PropType<(a: number, b: string) => { a: boolean }>,
        // explicit type casting with constructor
        ccc: Array as () => string[],
        // required + contructor type casting
        ddd: {
          type: Array as () => string[],
          required: true
        },
        // required + object return
        eee: {
          type: Function as PropType<() => { a: string }>,
          required: true
        },
        // required + arguments + object return
        fff: {
          type: Function as PropType<(a: number, b: string) => { a: boolean }>,
          required: true
        },
        hhh: {
          type: Boolean,
          required: true
        },
        // default + type casting
        ggg: {
          type: String as PropType<'foo' | 'bar'>,
          default: 'foo'
        },
        // default + function
        ffff: {
          type: Function as PropType<(a: number, b: string) => { a: boolean }>,
          default: (_a: number, _b: string) => ({ a: true })
        },
        validated: {
          type: String,
          // validator requires explicit annotation
          validator: (val: unknown) => val !== ''
        },
        object: Object as PropType<object>
      },

      setup() {
        return {
          setupA: 1
        }
      }
    } as const

    const { props, rawBindings, setup } = extractComponentOptions(MyComponent)

    // props
    expectType<ExpectedProps['a']>(props.a)
    expectType<ExpectedProps['b']>(props.b)
    expectType<ExpectedProps['e']>(props.e)
    expectType<ExpectedProps['bb']>(props.bb)
    expectType<ExpectedProps['bbb']>(props.bbb)
    expectType<ExpectedProps['cc']>(props.cc)
    expectType<ExpectedProps['dd']>(props.dd)
    expectType<ExpectedProps['ee']>(props.ee)
    expectType<ExpectedProps['ff']>(props.ff)
    expectType<ExpectedProps['ccc']>(props.ccc)
    expectType<ExpectedProps['ddd']>(props.ddd)
    expectType<ExpectedProps['eee']>(props.eee)
    expectType<ExpectedProps['fff']>(props.fff)
    expectType<ExpectedProps['hhh']>(props.hhh)
    expectType<ExpectedProps['ggg']>(props.ggg)
    // expectType<ExpectedProps['ffff']>(props.ffff) // todo fix
    expectType<ExpectedProps['validated']>(props.validated)
    expectType<ExpectedProps['object']>(props.object)

    // rawBindings
    expectType<Number>(rawBindings.setupA)

    //setup
    expectType<Number>(setup.setupA)
  })
})

describe('array props', () => {
  describe('defineComponent', () => {
    const MyComponent = defineComponent({
      props: ['a', 'b'],
      setup() {
        return {
          c: 1
        }
      }
    })

    const { props, rawBindings, setup } = extractComponentOptions(MyComponent)

    // @ts-expect-error props should be readonly
    expectError((props.a = 1))
    expectType<any>(props.a)
    expectType<any>(props.b)

    expectType<number>(rawBindings.c)
    expectType<number>(setup.c)
  })

  describe('options', () => {
    const MyComponent = {
      props: ['a', 'b'] as const,
      setup() {
        return {
          c: 1
        }
      }
    }

    const { props, rawBindings, setup } = extractComponentOptions(MyComponent)

    // @ts-expect-error props should be readonly
    expectError((props.a = 1))

    // TODO infer the correct keys
    // expectType<any>(props.a)
    // expectType<any>(props.b)

    expectType<number>(rawBindings.c)
    expectType<number>(setup.c)
  })
})

describe('no props', () => {
  describe('defineComponent', () => {
    const MyComponent = defineComponent({
      setup() {
        return {
          setupA: 1
        }
      }
    })

    const { rawBindings, setup } = extractComponentOptions(MyComponent)

    expectType<number>(rawBindings.setupA)
    expectType<number>(setup.setupA)

    // instance
    const instance = new MyComponent()
    expectType<number>(instance.setupA)
    // @ts-expect-error
    instance.notExist
  })

  describe('options', () => {
    const MyComponent = {
      setup() {
        return {
          setupA: 1
        }
      }
    }

    const { rawBindings, setup } = extractComponentOptions(MyComponent)

    expectType<number>(rawBindings.setupA)
    expectType<number>(setup.setupA)
  })
})

describe('functional', () => {
  // TODO `props.foo` is `number|undefined`
  //   describe('defineComponent', () => {
  //     const MyComponent = defineComponent((props: { foo: number }) => {})

  //     const { props } = extractComponentOptions(MyComponent)

  //     expectType<number>(props.foo)
  //   })

  describe('function', () => {
    const MyComponent = (props: { foo: number }) => props.foo
    const { props } = extractComponentOptions(MyComponent)

    expectType<number>(props.foo)
  })

  describe('typed', () => {
    const MyComponent: FunctionalComponent<{ foo: number }> = (_, _2) => {}

    const { props } = extractComponentOptions(MyComponent)

    expectType<number>(props.foo)
  })
})

declare type VueClass<Props = {}> = {
  new (): ComponentPublicInstance<Props>
}

describe('class', () => {
  const MyComponent: VueClass<{ foo: number }> = {} as any

  const { props } = extractComponentOptions(MyComponent)

  expectType<number>(props.foo)
})