import { Component, ComponentClass, mixins } from '@vue/runtime-core'
import { createInstance } from '@vue/runtime-test'

const calls: string[] = []

beforeEach(() => {
  calls.length = 0
})

class ClassMixinA extends Component<{ p1: string }, { d11: number }> {
  // props
  static props = {
    p1: String
  }

  // data
  d1 = 1
  data() {
    return {
      d11: 2
    }
  }

  // computed
  get c1() {
    return this.d1 + this.d11
  }

  // lifecycle
  created() {
    calls.push('created from mixin A')
  }

  // methods
  foo() {
    return this.d1
  }
}

class ClassMixinB extends Component<{ p2: string }, { d21: number }> {
  // props
  static props = {
    p2: String
  }

  // data
  d2 = 1
  data() {
    return {
      d21: 2
    }
  }

  get c2() {
    return this.d2 + this.d21
  }

  // lifecycle
  created() {
    calls.push('created from mixin B')
  }

  // methods
  bar() {
    return this.d2
  }
}

const ObjectMixinA = {
  props: {
    p1: String
  },
  data() {
    return {
      d1: 1,
      d11: 2
    }
  },
  computed: {
    c1() {
      return this.d1 + this.d11
    }
  },
  created() {
    calls.push('created from mixin A')
  },
  methods: {
    foo() {
      return this.d1
    }
  }
}

const ObjectMixinB = {
  props: {
    p2: String
  },
  data() {
    return {
      d2: 1,
      d21: 2
    }
  },
  computed: {
    c2() {
      return this.d2 + this.d21
    }
  },
  created() {
    calls.push('created from mixin B')
  },
  methods: {
    bar() {
      return this.d2
    }
  }
}

function assertMixins(Test: any) {
  const instance = createInstance(Test, {
    p1: '1',
    p2: '2',
    p3: '3'
  }) as any

  // data
  expect(instance.d1).toBe(1)
  expect(instance.d11).toBe(2)
  expect(instance.d2).toBe(1)
  expect(instance.d21).toBe(2)
  expect(instance.d3).toBe(1)
  expect(instance.d31).toBe(2)

  // props
  expect(instance.p1).toBe('1')
  expect(instance.p2).toBe('2')
  expect(instance.p3).toBe('3')
  expect(instance.$props.p1).toBe('1')
  expect(instance.$props.p2).toBe('2')
  expect(instance.$props.p3).toBe('3')

  // computed
  expect(instance.c1).toBe(3)
  expect(instance.c2).toBe(3)
  expect(instance.c3).toBe(3)

  // lifecycle
  expect(calls).toEqual([
    'created from mixin A',
    'created from mixin B',
    'created from Test'
  ])

  // methods
  expect(instance.foo()).toBe(1)
  expect(instance.bar()).toBe(1)
  expect(instance.baz()).toBe(1)
}

describe('mixins', () => {
  it('should work with classes', () => {
    class Test extends mixins(ClassMixinA, ClassMixinB)<
      { p3: string },
      { d31: number }
    > {
      static props = {
        p3: String
      }

      d3 = 1
      data(): any {
        return {
          d31: 2
        }
      }

      get c3() {
        return this.d3 + this.d31
      }

      created() {
        calls.push('created from Test')
      }

      baz() {
        return this.d3
      }
    }

    const instance = createInstance(Test, {
      p1: '1',
      p2: '2',
      p3: '3'
    })

    // we duplicate the assertions because they serve as type tests as well

    // data
    expect(instance.d1).toBe(1)
    expect(instance.d11).toBe(2)
    expect(instance.d2).toBe(1)
    expect(instance.d21).toBe(2)
    expect(instance.d3).toBe(1)
    expect(instance.d31).toBe(2)

    // props
    expect(instance.p1).toBe('1')
    expect(instance.p2).toBe('2')
    expect(instance.p3).toBe('3')
    expect(instance.$props.p1).toBe('1')
    expect(instance.$props.p2).toBe('2')
    expect(instance.$props.p3).toBe('3')

    // computed
    expect(instance.c1).toBe(3)
    expect(instance.c2).toBe(3)
    expect(instance.c3).toBe(3)

    // lifecycle
    expect(calls).toEqual([
      'created from mixin A',
      'created from mixin B',
      'created from Test'
    ])

    // methods
    expect(instance.foo()).toBe(1)
    expect(instance.bar()).toBe(1)
    expect(instance.baz()).toBe(1)
  })

  it('should work with objects', () => {
    class Test extends ((mixins as any)(
      ObjectMixinA,
      ObjectMixinB
    ) as ComponentClass)<{ p3: string }, { d31: number }> {
      static props = {
        p3: String
      }

      d3 = 1
      data(): any {
        return {
          d31: 2
        }
      }

      get c3() {
        return this.d3 + this.d31
      }

      created() {
        calls.push('created from Test')
      }

      baz() {
        return this.d3
      }
    }

    assertMixins(Test)
  })

  it('should work with a mix of objects and classes', () => {
    class Test extends ((mixins as any)(
      ClassMixinA,
      ObjectMixinB
    ) as ComponentClass)<{ p3: string }, { d31: number }> {
      static props = {
        p3: String
      }

      d3 = 1
      data(): any {
        return {
          d31: 2
        }
      }

      get c3() {
        return this.d3 + this.d31
      }

      created() {
        calls.push('created from Test')
      }

      baz() {
        return this.d3
      }
    }

    assertMixins(Test)
  })
})
