import { test } from '@japa/runner'
import { observed, retargetChange, toRaw } from '../../../src/decorators/observed.js'

test.group('observed decorator', () => {
  test('should decorate accessor property and track changes', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor testProperty: any
    }

    const instance = new TestClass()

    // Setting property should trigger domain notification
    instance.testProperty = 'test value'
    assert.equal(domainNotifyCallCount, 1)
    assert.equal(instance.testProperty, 'test value')

    // Setting same value should not trigger notification
    instance.testProperty = 'test value'
    assert.equal(domainNotifyCallCount, 1)

    // Setting different value should trigger notification
    instance.testProperty = 'new value'
    assert.equal(domainNotifyCallCount, 2)
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should use notifyChange method on instance if domain is not available', ({ assert }) => {
    let instanceNotifyCallCount = 0

    class TestClass {
      notifyChange() {
        instanceNotifyCallCount++
      }

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor testProperty: any
    }

    const instance = new TestClass()

    instance.testProperty = 'test value'
    assert.equal(instanceNotifyCallCount, 1)
    assert.equal(instance.testProperty, 'test value')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should work with setter decorator', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      #value: string | undefined
      domain = mockDomain

      @observed()
      set testProperty(value: string | undefined) {
        this.#value = value
      }

      get testProperty() {
        return this.#value
      }
    }

    const instance = new TestClass()

    instance.testProperty = 'test value'
    assert.equal(domainNotifyCallCount, 1)
    assert.equal(instance.testProperty, 'test value')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should support deep observation of objects', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor nestedData: any
    }

    const instance = new TestClass()
    const testObj = { nested: { value: 'initial' } }

    // Setting object should trigger notification
    instance.nestedData = testObj
    assert.equal(domainNotifyCallCount, 1)

    // Modifying deep property should trigger notification
    instance.nestedData.nested.value = 'changed'
    assert.equal(domainNotifyCallCount, 2)
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle primitive values with deep observation', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor primitiveValue: any
    }

    const instance = new TestClass()

    // Setting primitive values should work normally
    instance.primitiveValue = 42
    assert.equal(domainNotifyCallCount, 1)
    assert.equal(instance.primitiveValue, 42)

    instance.primitiveValue = 'string'
    assert.equal(domainNotifyCallCount, 2)
    assert.equal(instance.primitiveValue, 'string')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle null and undefined values correctly', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor nullableValue: any
    }

    const instance = new TestClass()

    instance.nullableValue = null
    assert.equal(domainNotifyCallCount, 1)
    assert.equal(instance.nullableValue, null)

    instance.nullableValue = undefined
    assert.equal(domainNotifyCallCount, 2)
    assert.equal(instance.nullableValue, undefined)

    instance.nullableValue = 'value'
    assert.equal(domainNotifyCallCount, 3)
    assert.equal(instance.nullableValue, 'value')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should throw error for unsupported decorator locations', ({ assert }) => {
    assert.throws(() => {
      const decorator = observed()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      decorator({} as any, { kind: 'field' } as any)
    }, 'Unsupported decorator location: field')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should maintain proxy references correctly with deep observation', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor complexData: any
    }

    const instance = new TestClass()
    const originalObj = { items: [{ name: 'item1' }] }

    instance.complexData = originalObj
    assert.equal(domainNotifyCallCount, 1)

    // Modifying array items should trigger notifications
    instance.complexData.items.push({ name: 'item2' })
    assert.equal(domainNotifyCallCount, 2)

    // Note: the deep proxy system returns original objects for already-accessed objects
    // so this modification may not trigger additional notifications
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle class without domain or notifyChange', ({ assert }) => {
    class TestClass {
      // @ts-expect-error It is for testing purposes
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor testProperty: any
    }

    const instance = new TestClass()

    // Should not throw error when setting value
    assert.doesNotThrow(() => {
      instance.testProperty = 'test value'
    })
    assert.equal(instance.testProperty, 'test value')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle multiple observed properties on same instance', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor prop1: any
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor prop2: any
    }

    const instance = new TestClass()

    instance.prop1 = 'value1'
    assert.equal(domainNotifyCallCount, 1)

    instance.prop2 = 'value2'
    assert.equal(domainNotifyCallCount, 2)

    assert.equal(instance.prop1, 'value1')
    assert.equal(instance.prop2, 'value2')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle circular references in deep observation', ({ assert }) => {
    let notifyChangeCallCount = 0
    const mockDomain = {
      notifyChange: () => {
        notifyChangeCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor data: any
    }

    const instance = new TestClass()
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const obj: any = { name: 'test' }
    obj.self = obj // Create circular reference

    // Should not crash with circular reference
    assert.doesNotThrow(() => {
      instance.data = obj
    })

    assert.equal(notifyChangeCallCount, 1)
    assert.equal(instance.data.name, 'test')
    // Circular reference is preserved but proxy system prevents infinite loops
    assert.equal(instance.data.self.name, 'test')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
})

test.group('retargetChange decorator', () => {
  class MockEventTarget {
    listeners: ((event: Event) => void)[] = []

    addEventListener(type: string, listener: (event: Event) => void) {
      if (type === 'change') {
        this.listeners.push(listener)
      }
    }

    removeEventListener(type: string, listener: (event: Event) => void) {
      if (type === 'change') {
        const index = this.listeners.indexOf(listener)
        if (index > -1) {
          this.listeners.splice(index, 1)
        }
      }
    }

    triggerChange() {
      const event = new Event('change')
      this.listeners.forEach((listener) => listener(event))
    }
  }

  test('should retarget change events from EventTarget properties', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      @retargetChange()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      accessor target: any
    }

    const instance = new TestClass()
    const mockTarget = new MockEventTarget()

    // Set EventTarget (accessor version notifies)
    instance.target = mockTarget
    assert.equal(domainNotifyCallCount, 1)

    // Trigger change event should notify domain
    mockTarget.triggerChange()
    assert.equal(domainNotifyCallCount, 2)
  }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])

  test('should handle setting same EventTarget value', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      @retargetChange()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      accessor target: any
    }

    const instance = new TestClass()
    const mockTarget = new MockEventTarget()

    // Set EventTarget
    instance.target = mockTarget
    assert.equal(domainNotifyCallCount, 1)

    // Set same EventTarget should not notify again
    instance.target = mockTarget
    assert.equal(domainNotifyCallCount, 1)
  }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])

  test('should work with setter decorator', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      #target: any
      domain = mockDomain

      @retargetChange()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      set target(value: any) {
        this.#target = value
      }

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      get target(): any {
        return this.#target
      }
    }

    const instance = new TestClass()
    const mockTarget = new MockEventTarget()

    // Set EventTarget (setter doesn't notify by itself)
    instance.target = mockTarget
    assert.equal(domainNotifyCallCount, 0)

    // Trigger change event should notify domain
    mockTarget.triggerChange()
    assert.equal(domainNotifyCallCount, 1)
  }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])

  test('should throw error for unsupported decorator locations', ({ assert }) => {
    assert.throws(() => {
      const decorator = retargetChange()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      decorator({} as any, { kind: 'field' } as any)
    }, 'Unsupported decorator location: field')
  }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])
})

test.group('toRaw function', () => {
  test('should return raw object from proxy', ({ assert }) => {
    const mockDomain = {
      notifyChange() {
        // ...
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor data: any
    }

    const instance = new TestClass()
    const originalObj = { name: 'test' }

    instance.data = originalObj

    // toRaw should return the original object
    const raw = toRaw(instance, instance.data)
    assert.strictEqual(raw, originalObj)
  }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])

  test('should return undefined if no proxy found', ({ assert }) => {
    const instance = {}
    const nonProxiedObj = { name: 'test' }

    const raw = toRaw(instance, nonProxiedObj)
    assert.equal(raw, undefined)
  }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])

  test('should return undefined if proxy symbol not found', ({ assert }) => {
    const instance = {}
    const obj = { name: 'test' }

    const raw = toRaw(instance, obj)
    assert.equal(raw, undefined)
  }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])
})

test.group('edge cases and error conditions', () => {
  test('should handle class without domain or notifyChange', ({ assert }) => {
    class TestClass {
      // @ts-expect-error It is for testing purposes
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor testProperty: any
    }

    const instance = new TestClass()

    // Should not throw error when setting value
    assert.doesNotThrow(() => {
      instance.testProperty = 'test value'
    })
    assert.equal(instance.testProperty, 'test value')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle multiple observed properties on same instance', ({ assert }) => {
    let domainNotifyCallCount = 0
    const mockDomain = {
      notifyChange() {
        domainNotifyCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor prop1: any
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed() accessor prop2: any
    }

    const instance = new TestClass()

    instance.prop1 = 'value1'
    assert.equal(domainNotifyCallCount, 1)

    instance.prop2 = 'value2'
    assert.equal(domainNotifyCallCount, 2)

    assert.equal(instance.prop1, 'value1')
    assert.equal(instance.prop2, 'value2')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])

  test('should handle circular references in deep observation', ({ assert }) => {
    let notifyChangeCallCount = 0
    const mockDomain = {
      notifyChange: () => {
        notifyChangeCallCount++
      },
    }

    class TestClass {
      domain = mockDomain

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      @observed({ deep: true }) accessor data: any
    }

    const instance = new TestClass()
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const obj: any = { name: 'test' }
    obj.self = obj // Create circular reference

    // Should not crash with circular reference
    assert.doesNotThrow(() => {
      instance.data = obj
    })

    assert.equal(notifyChangeCallCount, 1)
    assert.equal(instance.data.name, 'test')
    // Circular reference is preserved but proxy system prevents infinite loops
    assert.equal(instance.data.self.name, 'test')
  }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
})
