import { describe, it, beforeEach, expect, vi } from 'vitest'
import { AppTestHarness } from './Helpers/AppTestHarness'
import { LoginRegisterPresenter } from "./Helpers/Authentication/LoginRegisterPresenter";
import { LoginRegisterPresenterAuto } from "./Helpers/Authentication/LoginRegisterPresenterAuto";
import { notify } from "../useMobX.js";
import { isReactive } from "vue";
import { ObservableMap, ObservableSet } from "mobx";

let appTestHarness = null
let presenter = null
let vm = null

function wait () {
  return setTimeout(() => {})
}

function test (presenterClass, mode = 'manual') {

  describe('----- mode: '+mode+ '----', () => {

    beforeEach(() => {
      appTestHarness = new AppTestHarness()
      appTestHarness.init()
      presenter = appTestHarness.container.get(presenterClass)
      vm = presenter.vm
    })

    describe('------------------- Object Shadowing ------------------', () => {

      if (mode === 'manual') {

        it('parent non-observed properties are accessible', () => {
          expect(vm.nonObservedProp).toBe('test')
        })

        it('parent observed properties are accessible', () => {
          expect(vm.showValidationWarning).toBe(false)
        })

        it('presenter observed properties accessible', () => {
          expect(vm.email).toBe('test@test.ca')
        })

        it('grandparent properties are accessible', () => {
          expect(vm.grandParentProp).toBe(1)
        })


      }



      it('presenter non-observed properties accessible', () => {
        expect(vm.testNonObservable).toBe(1)
      })

      it('presenter non-observed simple properties reassignable', () => {
        vm.testNonObservable = 2
        expect(presenter.testNonObservable).toBe(2)
      })

      it('presenter non-observed object properties reassignable', () => {
        vm.testNonObservableObject = { test: 1 }
        expect(presenter.testNonObservableObject.test).toBe(1)
      })

      it('vm non-observed object properties are reassignable', () => {
        presenter.testNonObservableObject = { test: 1 }
        expect(vm.testNonObservableObject.test).toBe(1)
      })

      it('presenter non-observed object sub-properties are reassignable', () => {
        vm.testNonObservableObject.prop.prop.prop = 3
        expect(presenter.testNonObservableObject.prop.prop.prop).toBe(3)
      })

      it('presenter non-observed object sub-properties are reassignable', () => {
        vm.testNonObservableObject.prop.prop.prop = 3
        expect(presenter.testNonObservableObject.prop.prop.prop).toBe(3)
      })
    })

    describe('------------------- MobX Reactivity ------------------', () => {

      describe('mobX observable state propagation to vue', () => {

        describe('mobX change primitive', () => {

          it('mobX change string', () => {
            presenter._string = 'hello'
            expect(vm._string).toBe('hello')
          })

          it('mobX change boolean', () => {
            presenter._boolean = false
            expect(vm._boolean).toBe(false)
          })

          it('mobX change number', () => {
            presenter._number = 2
            expect(vm._number).toBe(2)
          })
        })

        describe('mobX change array', () => {

          describe('mobX change array at root', () => {

            it('mobX push to array root', () => {
              presenter._array.push(1)
              presenter._array.push(2)
              expect(vm._array[0]).toBe(1)
              expect(vm._array[1]).toBe(2)
            })

            it('mobX splice array at root', () => {
              presenter._arraySplice.splice(1, 0, 2)
              expect(vm._arraySplice[1]).toBe(2)
            })

            it('mobX unshift array at root', () => {
              presenter._arrayUnshift.unshift(0)
              expect(vm._arrayUnshift[0]).toBe(0)
            })

            it('mobX pop array at root', () => {
              presenter._arrayPop.pop()
              expect(vm._arrayPop[vm._arrayPop.length - 1]).toBe(1)
            })

            it('mobX shift array at root', () => {
              presenter._arrayShift.shift()
              expect(vm._arrayShift[0]).toBe(2)
            })
          })

          describe('mobX change deep nested array', () => {

            it('mobX push to nested array', () => {
              presenter._narray[0][0].push(1)
              presenter._narray[0][0].push(2)
              expect(vm._narray[0][0][0]).toBe(1)
              expect(vm._narray[0][0][1]).toBe(2)
            })

            it('mobX splice nested array', () => {
              presenter._narraySplice[0][0].splice(1, 0, 2)
              expect(vm._narraySplice[0][0][1]).toBe(2)
            })

            it('mobX unshift nested array', () => {
              presenter._narrayUnshift[0][0].unshift(0)
              expect(vm._narrayUnshift[0][0][0]).toBe(0)
            })

            it('mobX pop nested array', () => {
              presenter._narrayPop[0][0].pop()
              expect(vm._narrayPop[0][0][vm._narrayPop.length - 1]).toBe(1)
            })

            it('mobX shift nested array', () => {
              presenter._narrayShift[0][0].shift()
              expect(vm._narrayShift[0][0][0]).toBe(2)
            })
          })
        })

        describe('mobX change object', () => {

          describe('mobX change root props', () => {

            it('mobX add object prop to root', () => {
              presenter._object.prop = 1
              expect(vm._object.prop).toBe(1)
            })

            it('mobX update object prop at root', () => {
              presenter._objectUpdate.prop = 2
              expect(vm._objectUpdate.prop).toBe(2)
            })

            it('mobX delete object prop at root', () => {
              delete presenter._objectDelete.prop
              expect(vm._objectDelete.prop).toBeUndefined()
            })
          })

          describe('mobX change deep nested props', () => {

            it('mobX add nested object prop', () => {
              presenter._nobject.nest.nest.prop = 1
              expect(vm._nobject.nest.nest.prop).toBe(1)
            })

            it('mobX update nested object prop', () => {
              presenter._nobjectUpdate.nest.nest.prop = 2
              expect(vm._nobjectUpdate.nest.nest.prop).toBe(2)
            })

            it('mobX delete nested object prop', () => {
              delete presenter._nobjectDelete.nest.nest.prop
              expect(vm._nobjectDelete.nest.nest.prop).toBeUndefined()
            })
          })

        })

        describe('mobX object/array reassignment & maintain reactivity', () => {

          it('mobX reassign array at root', async () => {
            presenter._arrayReassign = [{ newProp: 1 }]
            expect(vm._arrayReassign[0].newProp).toBe(1)
            vm._arrayReassign[0].newProp = 2
            await wait()
            expect(presenter._arrayReassign[0].newProp).toBe(2)
          })

          it('mobX reassign whole observable object at root', async () => {
            presenter._objectReassign = { prop: { prop: true } }
            expect(vm._objectReassign.prop.prop).toBe(true)
            vm._objectReassign.prop.prop = false
            await wait()
            expect(presenter._objectReassign.prop.prop).toBe(false)
          })
        })
      })

      describe('mobX getter propagation', () => {

        describe('mobX getter propagation for array', () => {

          it('mobX push to array root', () => {
            presenter._getterArray.push({ prop: 1, prop2: 2 })
            expect(vm.getterArray[0].prop).toBe(1)
          })

          it('mobX change object in array', () => {
            presenter._getterNestedArray[0].prop = 3
            presenter._getterNestedArray[0].prop2 = 3
            expect(vm.getterNestedArray[0].prop).toBe(3)
            expect(vm.getterNestedArray[0].prop2).toBe(3)
          })

          it('mobX change nested object in array & notify manually', () => {
            presenter._getterNestedArray[0].prop3.prop = 4
            presenter._getterNestedArray[0].prop3 = notify(presenter._getterNestedArray[0].prop3)
            expect(vm.getterNestedArray[0].prop3.prop).toBe(4)
          })
        })

        describe('mobX getter propagation for object', () => {

          it('mobX add property to object',  () => {
            presenter._getterObject.prop2 = 3
            presenter._getterObject = notify(presenter._getterObject)
            expect(vm.getterObject.prop2).toBe(3)
          })

          it('mobX update property on nested object & notify manually', () => {
            presenter._getterNestedObject.prop3.prop = 3
            presenter._getterNestedObject = notify(presenter._getterNestedObject)
            expect(vm.getterNestedObject.prop3.prop).toBe(3)
          })
        })
      })

      describe('mobX Map/Set propagation', () => {

        describe('mobX Map propagation', () => {

          it('mobX set key & value into a Map', async () => {
            presenter.mapObject.set(1, 'foo')
            expect(vm.mapObject.get(1)).toBe('foo')
          })

          it('mobX delete from Map', () => {
            presenter.mapObject.set(1, 'foo')
            presenter.mapObject.delete(1)
            expect(vm.mapObject.get(1)).toBeUndefined()
          })
        })

        describe('mobX Set propagation', () => {

          it('mobX add to Set', async () => {
            const obj = { prop: 1, prop2: 2 }
            presenter.setObject.add(obj)
            vm.setObject.forEach(o => {
              expect(o).toEqual(obj)
            })
          })

          it('mobX delete from Set', () => {
            const obj = { prop: 1, prop2: 2 }
            presenter.setObject.add(obj)
            presenter.setObject.forEach(o => {
              presenter.setObject.delete(o)
            })

            let i = 0;
            vm.setObject.forEach(o => {
              i++
            })
            expect(i).toBe(0)
          })

        })

      })

      describe('mobx object/array reassignment & maintain reactivity', async () => {

        it('mobx reassign array at root', async () => {
          presenter._arrayReassign = [{ newProp: 1 }]
          expect(vm._arrayReassign[0].newProp).toBe(1)

          vm._arrayReassign[0].newProp = 2
          expect(vm._arrayReassign[0].newProp).toBe(2)
        })

        it('mobx reassign whole observable object at root', async () => {
          presenter._objectReassign = { prop: { prop: true } }
          expect(vm._objectReassign.prop.prop).toBe(true)

          vm._objectReassign.prop.prop = false
          expect(vm._objectReassign.prop.prop).toBe(false)
        })

        it('mobx reassign Map', async () => {
          presenter.mapObject = new Map()
          presenter.mapObject.set('prop', 1)
          expect(vm.mapObject.get('prop')).toBe(1)

          vm.mapObject.set('prop', 2)
          await wait()
          expect(presenter.mapObject.get('prop')).toBe(2)
        })

        it('mobx reassign Set', async () => {
          presenter.setObject = new Set()
          presenter.setObject.add(1)
          presenter.setObject.add(2)
          presenter.setObject.add(3)

          let i = 0, value

          const iterator = vm.setObject.entries();
          for (const entry of iterator) {
            i++
            value = entry
          }
          expect(i).toBe(3)
          expect(value[0]).toBe(3)

        })
      })


      it('--------------------- Vue Reactivity ---------------------', () => {})

      describe('vue state propagation to mobX', () => {

        describe('vue change primitive', () => {

          it('vue change string', async () => {
            vm._string = 'hello'
            await wait()
            expect(presenter._string).toBe('hello')
          })

          it('vue change boolean', async () => {
            vm._boolean = false
            await wait()
            expect(presenter._boolean).toBe(false)
          })

          it('vue change number', async () => {
            vm._number = 2
            await wait()
            expect(presenter._number).toBe(2)
          })
        })

        describe('vue change array', async () => {

          describe('vue change array at root', async () => {

            it('vue push to array root', async () => {
              vm._array.push(1)
              vm._array.push(2)
              await wait()
              expect(presenter._array[0]).toBe(1)
              expect(presenter._array[1]).toBe(2)
            })

            it('vue splice array at root', async () => {
              vm._arraySplice.splice(1, 0, 2)
              await wait()
              expect(presenter._arraySplice[1]).toBe(2)
            })

            it('vue unshift array at root', async () => {
              vm._arrayUnshift.unshift(0)
              await wait()
              expect(presenter._arrayUnshift[0]).toBe(0)
            })

            it('vue pop array at root', async () => {
              vm._arrayPop.pop()
              await wait()
              expect(presenter._arrayPop[presenter._arrayPop.length - 1]).toBe(1)
            })

            it('vue shift array at root', async () => {
              vm._arrayShift.shift()
              await wait()
              expect(presenter._arrayShift[0]).toBe(2)
            })
          })

          describe('vue change deep nested array', async () => {

            it('vue push to nested array', async () => {
              vm._narray[0][0].push(1)
              vm._narray[0][0].push(2)
              await wait()
              expect(presenter._narray[0][0][0]).toBe(1)
              expect(presenter._narray[0][0][1]).toBe(2)
            })

            it('vue splice nested array', async () => {
              vm._narraySplice[0][0].splice(1, 0, 2)
              await wait()
              expect(presenter._narraySplice[0][0][1]).toBe(2)
            })

            it('vue unshift nested array', async () => {
              vm._narrayUnshift[0][0].unshift(0)
              await wait()
              expect(presenter._narrayUnshift[0][0][0]).toBe(0)
            })

            it('vue pop nested array', async () => {
              vm._narrayPop[0][0].pop()
              await wait()
              expect(presenter._narrayPop[0][0][presenter._narrayPop.length - 1]).toBe(1)
            })

            it('vue shift nested array', async () => {
              vm._narrayShift[0][0].shift()
              await wait()
              expect(presenter._narrayShift[0][0][0]).toBe(2)
            })
          })
        })

        describe('vue change object', async () => {

          describe('vue change root props', async () => {

            it('vue add object prop to root', async () => {
              vm._object.prop = 1
              await wait()
              expect(presenter._object.prop).toBe(1)
            })

            it('vue update object prop at root', async () => {
              vm._objectUpdate.prop = 2
              await wait()
              expect(presenter._objectUpdate.prop).toBe(2)
            })

            it('vue delete object prop at root', async () => {
              delete vm._objectDelete.prop
              await wait()
              expect(presenter._objectDelete.prop).toBeUndefined()
            })
          })

          describe('vue change deep nested props', async () => {

            it('vue add nested object prop', async () => {
              vm._nobject.nest.nest.prop = 1
              await wait()
              expect(presenter._nobject.nest.nest.prop).toBe(1)
            })

            it('vue update nested object prop', async () => {
              vm._nobjectUpdate.nest.nest.prop = 2
              await wait()
              expect(presenter._nobjectUpdate.nest.nest.prop).toBe(2)
            })

            it('vue delete nested object prop', async () => {
              delete vm._nobjectDelete.nest.nest.prop
              await wait()
              expect(presenter._nobjectDelete.nest.nest.prop).toBeUndefined()
            })
          })
        })

        describe('vue object/array reassignment & maintain reactivity', async () => {

          it('vue reassign array at root', async () => {
            vm._arrayReassign = [{ newProp: 1 }]
            await wait()
            expect(presenter._arrayReassign[0].newProp).toBe(1)
            presenter._arrayReassign[0].newProp = 2
            expect(vm._arrayReassign[0].newProp).toBe(2)
          })

          it('vue reassign whole observable object at root', async () => {
            vm._objectReassign = { prop: { prop: true } }
            await wait()
            expect(presenter._objectReassign.prop.prop).toBe(true)

            presenter._objectReassign.prop.prop = false
            expect(vm._objectReassign.prop.prop).toBe(false)
          })

          it('vue reassign Map', async () => {
            vm.mapObject = new ObservableMap([[
              'prop', 1
            ]])
            await wait()
            expect(presenter.mapObject.get('prop')).toBe(1)

            presenter.mapObject.set('prop', 2)
            expect(vm.mapObject.get('prop')).toBe(2)
          })

          it('vue reassign Set', async () => {
            vm.setObject = new ObservableSet([1, 2])
            await wait()
            presenter.setObject.add(3)
            let i = 0
            presenter.setObject.forEach(el => {
              i++
              if (i === 3) {
                expect(el).toBe(3)
              }
            })

            let j = 0;
            vm.setObject.forEach(el => {
              j++
              if (j === 3) {
                expect(el).toBe(3)
              }
            })
          })
        })

        describe('vue Map/Set propagation', () => {

          describe('vue Map propagation', () => {

            it('vue add to Map', async () => {
              const obj = { prop: 1, prop2: 2 }
              vm.mapObject.set(1, 'bar')
              await wait()
              expect(presenter.mapObject.get(1)).toBe('bar')

              presenter.mapObject.set(1, 'foo')
              expect(vm.mapObject.get(1)).toBe('foo')
              expect(isReactive(vm.mapObject)).toBe(true)
            })

            it('vue delete from Map', async () => {
              vm.mapObject.set(1, 'foo')
              expect(isReactive(vm.mapObject)).toBe(true)
              await wait()
              expect(presenter.mapObject.get(1)).toBe('foo')
              vm.mapObject.delete(1)
              await wait()
              expect(presenter.mapObject.get(1)).toBeUndefined()
            })

          })

          describe('vue Set propagation', () => {

            it('vue add to Set', async () => {
              const obj = { prop: 1, prop2: 2 }
              vm.setObject.add(obj)
              presenter.setObject.forEach(o => {
                expect(o).toEqual(obj)
              })
            })

            it('vue delete from Set', () => {
              const obj = { prop: 1, prop2: 2 }
              vm.setObject.add(obj)
              vm.setObject.forEach(o => {
                vm.setObject.delete(o)
              })

              let i = 0;
              presenter.setObject.forEach(o => {
                i++
              })
              expect(i).toBe(0)
            })
          })
        })


      })
    })
  })
}

test(LoginRegisterPresenter, 'manual')
test(LoginRegisterPresenterAuto, 'auto')
