import { assert } from '@open-wc/testing'
import sinon from 'sinon'
import { LiveData } from '../../src/reactive/LiveData.js'

describe('LiveData Basic', () => {
  it('should initialize with the given object', () => {
    const data = { name: 'John Doe', age: 30 }
    const liveData = new LiveData(data)
    assert.deepEqual(liveData.value, data)
  })

  it('should update the value when a property is changed', () => {
    const data = { name: 'John Doe', age: 30 }
    const liveData = new LiveData(data)
    liveData.value.name = 'Jane Doe'
    assert.equal(liveData.value.name, 'Jane Doe')
  })

  it('should dispatch an update event when a property is changed', () => {
    const data = { name: 'John Doe', age: 30 }
    const liveData = new LiveData(data)
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.name = 'Jane Doe'

    assert.isTrue(spy.calledOnce)
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'name')
    assert.equal(args.value, 'Jane Doe')
  })

  it('should track the last changed path', () => {
    const data = { name: 'John Doe', address: { city: 'New York' } }
    const liveData = new LiveData(data)

    liveData.value.name = 'Jane Doe'
    assert.equal(liveData.lastChangedPath, 'name')
    liveData.value.address.city = 'Los Angeles'
    assert.equal(liveData.lastChangedPath, 'address.city')
  })

  it('should handle nested objects', () => {
    const data = { user: { name: 'John Doe' } }
    const liveData = new LiveData(data)
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.user.name = 'Jane Doe'
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'user.name')
    assert.equal(args.value, 'Jane Doe')
  })

  it('should handle arrays', () => {
    const data = { items: ['apple', 'banana'] }
    const liveData = new LiveData(data)

    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.items.push('orange')

    assert.equal(spy.callCount, 1, 'called once for the push')
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'items.2', 'path is correct')
    assert.equal(args.value, 'orange', 'value is correct')
  })

  it('should handle sets', () => {
    const data = { set: new Set(['apple', 'banana']) }
    const liveData = new LiveData(data)

    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.set.add('orange')

    assert.equal(spy.callCount, 1, 'called once for the add')
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'set', 'path is correct')
    assert.deepEqual(args.value, data.set, 'value is correct')
  })

  it('should handle maps', () => {
    const data = { map: new Map([['a', 'b']]) }
    const liveData = new LiveData(data)

    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.map.set('c', 'd')

    assert.equal(spy.callCount, 1, 'called once for the add')
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'map', 'path is correct')
    assert.deepEqual(args.value, data.map, 'value is correct')
  })

  it('should handle arrays with array index', () => {
    const data = { items: ['apple', 'banana'] }
    const liveData = new LiveData(data)

    const spy = sinon.spy()
    liveData.addEventListener('update', spy)

    liveData.value.items[0] = 'orange'

    assert.equal(spy.callCount, 1)
    const args = spy.args[0][0].detail
    assert.equal(args.path, 'items.0')
    assert.equal(args.value, 'orange')
  })

  // it('should call parent callback when child LiveData is updated', () => {
  //   const childData = new LiveData({ value: 'Initial Value' })
  //   const parentData = { child: childData }
  //   const parentLiveData = new LiveData(parentData)

  //   const spy = sinon.spy()
  //   parentLiveData.addEventListener('update', spy)

  //   childData.value.value = 'New Value'

  //   assert.equal(spy.callCount, 1)
  //   const args = spy.args[0][0].detail
  //   assert.equal(args.path, 'child.value')
  //   assert.equal(args.value, 'New Value')
  // })

  // it('should unhook child from parent when child LiveData is replaced', () => {
  //   const childData = new LiveData({ value: 'Initial Value' })
  //   const parentData = new LiveData({ child: childData })
  //   let parentUpdateCount = 0

  //   parentData.addEventListener('update', (event) => {
  //     parentUpdateCount++
  //     if (parentUpdateCount === 1) {
  //       assert.equal((event as CustomEvent).detail.path, 'child.value')
  //     } else if (parentUpdateCount === 2) {
  //       assert.equal((event as CustomEvent).detail.path, 'child')
  //     }
  //   })

  //   childData.value.value = 'New Value'

  //   // @ts-expect-error For testing
  //   parentData.value.child = { value: 'New Child' }
  //   childData.value.value = 'Another New Value'

  //   // the first one is when changing the name
  //   // and the second happens when replacing the child.
  //   assert.equal(parentUpdateCount, 2)
  // })

  // it('should unhook child from parent when child LiveData is removed', () => {
  //   const childData = new LiveData({ value: 'Initial Value' })
  //   const parentData = { child: childData }
  //   const parentLiveData = new LiveData(parentData)
  //   let parentUpdateCount = 0

  //   parentLiveData.addEventListener('update', (event) => {
  //     parentUpdateCount++
  //     if (parentUpdateCount === 1) {
  //       assert.equal((event as CustomEvent).detail.path, 'child.value')
  //     }
  //   })

  //   childData.value.value = 'New Value'

  //   // @ts-expect-error This is for testing.
  //   delete parentLiveData.value.child
  //   childData.value.value = 'Another New Value'

  //   assert.equal(parentUpdateCount, 1)
  // })

  // it('should unhook child from parent when removeParentCallback is called', () => {
  //   const childData = new LiveData({ value: 'Initial Value' })
  //   const parentData = { child: childData }
  //   const parentLiveData = new LiveData(parentData)
  //   let parentUpdateCount = 0

  //   parentLiveData.addEventListener('update', (event) => {
  //     parentUpdateCount++
  //     if (parentUpdateCount === 1) {
  //       assert.equal((event as CustomEvent).detail.path, 'child.value')
  //     }
  //   })

  //   childData.value.value = 'New Value'

  //   childData.removeParentCallback(parentLiveData)
  //   childData.value.value = 'Another New Value'

  //   assert.equal(parentUpdateCount, 1)
  // })

  // it('should filter updates with observeProperties', () => {
  //   const data = { name: 'John Doe', age: 30, city: 'New York' }
  //   const liveData = new LiveData(data)

  //   const spy = sinon.spy()
  //   const unsubscribe = liveData.observeProperties(['name', 'age'], spy)

  //   liveData.value.name = 'Jane Doe'
  //   liveData.value.age = 31
  //   liveData.value.city = 'Los Angeles'
  //   unsubscribe()
  //   liveData.value.name = 'Another New Name' // Should not trigger the callback
  //   assert.equal(spy.callCount, 2, 'has only requested changes')
  //   const args1 = spy.args[0][0]
  //   assert.equal(args1.path, 'name')
  //   assert.equal(args1.value, 'Jane Doe')
  //   assert.equal(args1.previousValue, 'John Doe')
  //   const args2 = spy.args[1][0]
  //   assert.equal(args2.path, 'age')
  //   assert.equal(args2.value, 31)
  //   assert.equal(args2.previousValue, 30)
  // })

  // it('should filter updates with observeProperties and deep properties', () => {
  //   const data = { user: { address: { city: 'New York' } } }
  //   const liveData = new LiveData(data)

  //   const spy = sinon.spy()
  //   const unsubscribe = liveData.observeProperties(['user.address.city'], spy)

  //   liveData.value.user.address.city = 'Los Angeles'
  //   unsubscribe()
  //   assert.equal(spy.callCount, 1, 'has only requested changes')
  //   const args1 = spy.args[0][0]
  //   assert.equal(args1.path, 'user.address.city')
  //   assert.equal(args1.value, 'Los Angeles')
  //   assert.equal(args1.previousValue, 'New York')
  // })

  // it('should filter updates with observeProperties and wildcard', () => {
  //   const data = { items: ['apple', 'banana'] }
  //   const liveData = new LiveData(data)

  //   const spy = sinon.spy()
  //   const unsubscribe = liveData.observeProperties(['items.*'], spy)

  //   liveData.value.items[0] = 'orange'
  //   liveData.value.items.push('pear')
  //   unsubscribe()

  //   assert.equal(spy.callCount, 2, 'has only requested changes')
  //   const args1 = spy.args[0][0]
  //   assert.equal(args1.path, 'items.0')
  //   assert.equal(args1.value, 'orange')
  //   assert.equal(args1.previousValue, 'apple')
  //   const args2 = spy.args[1][0]
  //   assert.equal(args2.path, 'items.2')
  //   assert.equal(args2.value, 'pear')
  //   assert.equal(args2.previousValue, undefined)
  // })

  it('should return correct value using get() with deep path', () => {
    const data = { user: { address: { city: 'New York' } } }
    const liveData = new LiveData(data)
    const city = liveData.get('user.address.city')
    assert.equal(city, 'New York')
  })

  it('should return correct value using get() with array index', () => {
    const data = { items: ['apple', 'banana'] }
    const liveData = new LiveData(data)
    const item0 = liveData.get('items.0')
    assert.equal(item0, 'apple')

    const item1 = liveData.get('items.1')
    assert.equal(item1, 'banana')
  })

  it('should return undefined using get() with wrong path', () => {
    const data = { user: { address: { city: 'New York' } } }
    const liveData = new LiveData(data)
    const street = liveData.get('user.address.street')
    assert.isUndefined(street)
  })

  // it('should handle replacing the entire object with set', () => {
  //   const initialData = { a: 1 } as { a?: number; b?: number }
  //   const liveData = new LiveData(initialData)

  //   const spy = sinon.spy()
  //   liveData.addEventListener('update', spy)

  //   liveData.set({ b: 2 })
  //   assert.isTrue(spy.calledOnce)
  //   const args = spy.args[0][0].detail
  //   assert.equal(args.path, '')
  //   assert.deepEqual(args.value, { b: 2 })
  //   assert.deepEqual(args.previousValue, { a: 1 })
  // })

  // it('should handle replacing the entire object with set and trigger observeProperties', () => {
  //   const initialData = { a: 1 } as { a?: number; b?: number }
  //   const liveData = new LiveData(initialData)

  //   const spy = sinon.spy()
  //   const unsubscribe = liveData.observeProperties(['a', 'b'], spy)

  //   liveData.set({ b: 2 })
  //   liveData.value.b = 3
  //   unsubscribe()

  //   assert.equal(spy.callCount, 1, 'has only requested changes')
  //   const args = spy.args[0][0]
  //   assert.equal(args.path, 'b')
  //   assert.equal(args.value, 3)
  //   assert.equal(args.previousValue, 2)
  // })
})

describe('LiveData Advanced', () => {
  beforeEach(() => {
    sinon.restore()
  })

  // it('createRecursiveProxy - Correct Passing of parentLiveData', () => {
  //   const parentLiveData = new LiveData({
  //     level1: {
  //       level2: {
  //         level3: new LiveData({ value: 'Initial Value' }),
  //       },
  //     },
  //   })

  //   const spy = sinon.spy()
  //   parentLiveData.addEventListener('update', spy)
  //   parentLiveData.value.level1.level2.level3.value.value = 'New Value'

  //   assert.isTrue(spy.calledOnce)
  //   const args = spy.args[0][0].detail
  //   assert.equal(args.path, 'level1.level2.level3.value')
  //   assert.equal(args.value, 'New Value')
  // })

  // it('createRecursiveProxy - Multiple Child LiveData', () => {
  //   const parentLiveData = new LiveData({
  //     child1: new LiveData({ value: 'Initial Value 1' }),
  //     child2: new LiveData({ value: 'Initial Value 2' }),
  //   })
  //   const spy = sinon.spy()
  //   parentLiveData.addEventListener('update', spy)

  //   parentLiveData.value.child1.value.value = 'New Value 1'
  //   assert.isTrue(spy.calledOnce)
  //   let args = spy.args[0][0].detail
  //   assert.equal(args.path, 'child1.value')
  //   assert.equal(args.value, 'New Value 1')

  //   parentLiveData.value.child2.value.value = 'New Value 2'
  //   assert.equal(spy.callCount, 2)
  //   args = spy.args[1][0].detail
  //   assert.equal(args.path, 'child2.value')
  //   assert.equal(args.value, 'New Value 2')
  // })

  it('createRecursiveProxy - Objects inside LiveData', () => {
    const parentLiveData = new LiveData({
      child1: { value: 'Initial Value 1' },
      child2: { nested: { value: 'Initial Nested Value 2' } },
    })
    const spy = sinon.spy()
    parentLiveData.addEventListener('update', spy)

    parentLiveData.value.child1.value = 'New Value 1'
    assert.isTrue(spy.calledOnce)
    let args = spy.args[0][0].detail
    assert.equal(args.path, 'child1.value')
    assert.equal(args.value, 'New Value 1')

    parentLiveData.value.child2.nested.value = 'New Value 2'
    assert.equal(spy.callCount, 2)
    args = spy.args[1][0].detail
    assert.equal(args.path, 'child2.nested.value')
    assert.equal(args.value, 'New Value 2')
  })
  it('observeProperties - Unsubscribe Handling', () => {
    const data = { name: 'John Doe' }
    const liveData = new LiveData(data)
    const spy = sinon.spy()

    const unsubscribe = liveData.observeProperties(['name'], spy)
    liveData.value.name = 'Jane Doe'
    assert.isTrue(spy.calledOnce)

    unsubscribe()
    liveData.value.name = 'Another New Name' // Should not trigger the callback
    assert.isTrue(spy.calledOnce)
  })

  // it('set() - Correct Behavior with Existing Child LiveData', () => {
  //   const childData = new LiveData({ value: 'Initial Value' })
  //   const parentData = new LiveData({ child: childData })
  //   const spy = sinon.spy()
  //   parentData.addEventListener('update', spy)

  //   parentData.value.child.value.value = 'New Value'
  //   assert.equal(spy.callCount, 1)

  //   // @ts-expect-error It is for testing.
  //   parentData.set({ child: { value: 'New Child' } })
  //   childData.value.value = 'Another New Value'
  //   // setting value should not trigger any change.

  //   assert.equal(spy.callCount, 2)
  //   assert.equal(spy.args[0][0].detail.path, 'child.value')
  //   assert.equal(spy.args[0][0].detail.value, 'New Value')
  //   assert.equal(spy.args[1][0].detail.path, '')
  //   assert.deepEqual(spy.args[1][0].detail.value, { child: { value: 'New Child' } })
  // })

  it('set() with nested objects', () => {
    const data = { user: { address: { city: 'New York' } } }
    const liveData = new LiveData(data)

    const spy = sinon.spy()
    liveData.addEventListener('update', spy)
    liveData.set({ user: { address: { city: 'New York2' } } })
    assert.equal(spy.callCount, 1)
    const args = spy.args[0][0].detail
    assert.equal(args.path, '')
    assert.deepEqual(args.value, { user: { address: { city: 'New York2' } } })
  })

  // it('Array length check', () => {
  //   const data = { items: ['apple', 'banana'] }
  //   const liveData = new LiveData(data)

  //   const spy = sinon.spy()
  //   liveData.addEventListener('update', spy)

  //   liveData.value.items.push('orange')
  //   assert.equal(spy.callCount, 1)
  //   assert.equal(spy.args[0][0].detail.path, 'items.2')

  //   liveData.value.items.length = 0
  //   assert.equal(spy.callCount, 1, 'no extra call for the `length`')
  // })

  it('get() with non existing nested properties', () => {
    const data = { user: { address: {} } }
    const liveData = new LiveData(data)
    const city = liveData.get('user.address.city')
    assert.isUndefined(city)
  })

  it('set() - should unhook LiveData instances in replaced object', () => {
    const childData1 = new LiveData({ value: 'Initial Value 1' })
    const childData2 = new LiveData({ value: 'Initial Value 2' })
    const initialData = {
      child1: childData1,
      child2: childData2,
    }
    const liveData = new LiveData(initialData)

    const child1Spy = sinon.spy()
    const child2Spy = sinon.spy()

    liveData.addEventListener('update', child1Spy)
    liveData.addEventListener('update', child2Spy)

    // We need to create a nested object to test the recursivelyRemoveParentCallbacks
    const newObject = {
      child1: { value: 'test' },
      child2: { value: 'test' },
    }

    // @ts-expect-error it is for testing
    liveData.set(newObject)

    childData1.value.value = 'Changed 1'
    childData2.value.value = 'Changed 2'

    assert.equal(child1Spy.callCount, 1)
    assert.equal(child2Spy.callCount, 1)
  })

  it('Should correctly handle multiple observeProperties on the same property', () => {
    const data = { value: 0 }
    const liveData = new LiveData(data)

    const spy1 = sinon.spy()
    const spy2 = sinon.spy()

    liveData.observeProperties(['value'], spy1)
    liveData.observeProperties(['value'], spy2)

    liveData.value.value = 1

    assert.equal(spy1.callCount, 1)
    assert.equal(spy2.callCount, 1)
    assert.equal(spy1.args[0][0].value, 1)
    assert.equal(spy2.args[0][0].value, 1)
  })

  it('Should correctly handle multiple observeProperties on different properties', () => {
    const data = { value1: 0, value2: 0 }
    const liveData = new LiveData(data)

    const spy1 = sinon.spy()
    const spy2 = sinon.spy()

    liveData.observeProperties(['value1'], spy1)
    liveData.observeProperties(['value2'], spy2)

    liveData.value.value1 = 1
    liveData.value.value2 = 2

    assert.equal(spy1.callCount, 1)
    assert.equal(spy2.callCount, 1)
    assert.equal(spy1.args[0][0].value, 1)
    assert.equal(spy2.args[0][0].value, 2)
  })

  it('Should handle setting null', () => {
    const liveData = new LiveData({ value: 0 })
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)
    // @ts-expect-error it is for testing
    liveData.value.value = null
    assert.equal(spy.callCount, 1)
    assert.equal(spy.args[0][0].detail.value, null)
  })
  it('Should handle setting undefined', () => {
    const liveData = new LiveData({ value: 0 })
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)
    // @ts-expect-error it is for testing
    liveData.value.value = undefined
    assert.equal(spy.callCount, 1)
    assert.isUndefined(spy.args[0][0].detail.value)
  })
  it('Should handle setting null nested', () => {
    const liveData = new LiveData({ user: { name: 'test' } })
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)
    // @ts-expect-error it is for testing
    liveData.value.user.name = null
    assert.equal(spy.callCount, 1)
    assert.equal(spy.args[0][0].detail.value, null)
  })

  it('Should handle setting undefined nested', () => {
    const liveData = new LiveData({ user: { name: 'test' } })
    const spy = sinon.spy()
    liveData.addEventListener('update', spy)
    // @ts-expect-error it is for testing
    liveData.value.user.name = undefined
    assert.equal(spy.callCount, 1)
    assert.isUndefined(spy.args[0][0].detail.value)
  })
})
