import { describe, test, expect } from 'bun:test'
import {
	state,
	computed,
	UNSET,
	isComputed,
	isState,
	effect,
} from '../index.ts'

/* === Utility Functions === */

const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
const increment = (n: number) => (Number.isFinite(n) ? n + 1 : UNSET)

/* === Tests === */

describe('Computed', function () {
	test('should identify computed signals with isComputed()', () => {
		const count = state(42)
		const doubled = computed(() => count.get() * 2)
		expect(isComputed(doubled)).toBe(true)
		expect(isState(doubled)).toBe(false)
	})

	test('should compute a function', function () {
		const derived = computed(() => 1 + 2)
		expect(derived.get()).toBe(3)
	})

	test('should compute function dependent on a signal', function () {
		const cause = state(42)
		const derived = computed(() => cause.get() + 1)
		expect(derived.get()).toBe(43)
	})

	test('should compute function dependent on an updated signal', function () {
		const cause = state(42)
		const derived = computed(() => cause.get() + 1)
		cause.set(24)
		expect(derived.get()).toBe(25)
	})

	test('should compute function dependent on an async signal', async function () {
		const status = state('pending')
		const promised = computed(async () => {
			await wait(100)
			status.set('success')
			return 42
		})
		const derived = computed(() => increment(promised.get()))
		expect(derived.get()).toBe(UNSET)
		expect(status.get()).toBe('pending')
		await wait(110)
		expect(derived.get()).toBe(43)
		expect(status.get()).toBe('success')
	})

	test('should handle errors from an async signal gracefully', async function () {
		const status = state('pending')
		const error = state('')
		const promised = computed(async () => {
			await wait(100)
			status.set('error')
			error.set('error occurred')
			return 0
		})
		const derived = computed(() => increment(promised.get()))
		expect(derived.get()).toBe(UNSET)
		expect(status.get()).toBe('pending')
		await wait(110)
		expect(error.get()).toBe('error occurred')
		expect(status.get()).toBe('error')
	})

	test('should compute task signals in parallel without waterfalls', async function () {
		const a = computed(async () => {
			await wait(100)
			return 10
		})
		const b = computed(async () => {
			await wait(100)
			return 20
		})
		const c = computed(() => {
			const aValue = a.get()
			const bValue = b.get()
			return aValue === UNSET || bValue === UNSET
				? UNSET
				: aValue + bValue
		})
		expect(c.get()).toBe(UNSET)
		await wait(110)
		expect(c.get()).toBe(30)
	})

	test('should compute function dependent on a chain of computed states dependent on a signal', function () {
		const x = state(42)
		const a = computed(() => x.get() + 1)
		const b = computed(() => a.get() * 2)
		const c = computed(() => b.get() + 1)
		expect(c.get()).toBe(87)
	})

	test('should compute function dependent on a chain of computed states dependent on an updated signal', function () {
		const x = state(42)
		const a = computed(() => x.get() + 1)
		const b = computed(() => a.get() * 2)
		const c = computed(() => b.get() + 1)
		x.set(24)
		expect(c.get()).toBe(51)
	})

	test('should drop X->B->X updates', function () {
		let count = 0
		const x = state(2)
		const a = computed(() => x.get() - 1)
		const b = computed(() => x.get() + a.get())
		const c = computed(() => {
			count++
			return 'c: ' + b.get()
		})
		expect(c.get()).toBe('c: 3')
		expect(count).toBe(1)
		x.set(4)
		expect(c.get()).toBe('c: 7')
		expect(count).toBe(2)
	})

	test('should only update every signal once (diamond graph)', function () {
		let count = 0
		const x = state('a')
		const a = computed(() => x.get())
		const b = computed(() => x.get())
		const c = computed(() => {
			count++
			return a.get() + ' ' + b.get()
		})
		expect(c.get()).toBe('a a')
		expect(count).toBe(1)
		x.set('aa')
		// flush()
		expect(c.get()).toBe('aa aa')
		expect(count).toBe(2)
	})

	test('should only update every signal once (diamond graph + tail)', function () {
		let count = 0
		const x = state('a')
		const a = computed(() => x.get())
		const b = computed(() => x.get())
		const c = computed(() => a.get() + ' ' + b.get())
		const d = computed(() => {
			count++
			return c.get()
		})
		expect(d.get()).toBe('a a')
		expect(count).toBe(1)
		x.set('aa')
		expect(d.get()).toBe('aa aa')
		expect(count).toBe(2)
	})

	test('should update multiple times after multiple state changes', function () {
		const a = state(3)
		const b = state(4)
		let count = 0
		const sum = computed(() => {
			count++
			return a.get() + b.get()
		})
		expect(sum.get()).toBe(7)
		a.set(6)
		expect(sum.get()).toBe(10)
		b.set(8)
		expect(sum.get()).toBe(14)
		expect(count).toBe(3)
	})

	/*
	 * Note for the next two tests:
	 *
	 * Due to the lazy evaluation strategy, unchanged computed signals may propagate
	 * change notifications one additional time before stabilizing. This is a
	 * one-time performance cost that allows for efficient memoization and
	 * error handling in most cases.
	 */
	test('should bail out if result is the same', function () {
		let count = 0
		const x = state('a')
		const a = computed(() => {
			x.get()
			return 'foo'
		})
		const b = computed(() => {
			count++
			return a.get()
		})
		expect(b.get()).toBe('foo')
		expect(count).toBe(1)
		x.set('aa')
		x.set('aaa')
		x.set('aaaa')
		expect(b.get()).toBe('foo')
		expect(count).toBe(2)
	})

	test('should block if result remains unchanged', function () {
		let count = 0
		const x = state(42)
		const a = computed(() => x.get() % 2)
		const b = computed(() => (a.get() ? 'odd' : 'even'))
		const c = computed(() => {
			count++
			return `c: ${b.get()}`
		})
		expect(c.get()).toBe('c: even')
		expect(count).toBe(1)
		x.set(44)
		x.set(46)
		x.set(48)
		expect(c.get()).toBe('c: even')
		expect(count).toBe(2)
	})

	test('should detect and throw error for circular dependencies', function () {
		const a = state(1)
		const b = computed(() => c.get() + 1)
		const c = computed(() => b.get() + a.get())
		expect(() => {
			b.get() // This should trigger the circular dependency
		}).toThrow('Circular dependency in computed detected')
		expect(a.get()).toBe(1)
	})

	test('should propagate error if an error occurred', function () {
		let okCount = 0
		let errCount = 0
		const x = state(0)
		const a = computed(() => {
			if (x.get() === 1) throw new Error('Calculation error')
			return 1
		})

		// Replace matcher with try/catch in a computed
		const b = computed(() => {
			try {
				a.get() // just check if it works
				return `c: success`
			} catch (_error) {
				errCount++
				return `c: recovered`
			}
		})
		const c = computed(() => {
			okCount++
			return b.get()
		})

		expect(a.get()).toBe(1)
		expect(c.get()).toBe('c: success')
		expect(okCount).toBe(1)
		try {
			x.set(1)
			expect(a.get()).toBe(1)
			expect(true).toBe(false) // This line should not be reached
		} catch (error) {
			expect(error.message).toBe('Calculation error')
		} finally {
			expect(c.get()).toBe('c: recovered')
			expect(okCount).toBe(2)
			expect(errCount).toBe(1)
		}
	})

	test('should create an effect that reacts on async computed changes', async function () {
		const cause = state(42)
		const derived = computed(async () => {
			await wait(100)
			return cause.get() + 1
		})
		let okCount = 0
		let nilCount = 0
		let result: number = 0
		effect({
			signals: [derived],
			ok: v => {
				result = v
				okCount++
			},
			nil: () => {
				nilCount++
			},
		})
		cause.set(43)
		expect(okCount).toBe(0)
		expect(nilCount).toBe(1)
		expect(result).toBe(0)

		await wait(110)
		expect(okCount).toBe(1) // not +1 because initial state never made it here
		expect(nilCount).toBe(1)
		expect(result).toBe(44)
	})

	test('should handle complex computed signal with error and async dependencies', async function () {
		const toggleState = state(true)
		const errorProne = computed(() => {
			if (toggleState.get()) throw new Error('Intentional error')
			return 42
		})
		const asyncValue = computed(async () => {
			await wait(50)
			return 10
		})
		let okCount = 0
		let nilCount = 0
		let errCount = 0
		// let _result: number = 0

		const complexComputed = computed(() => {
			try {
				const x = errorProne.get()
				const y = asyncValue.get()
				if (y === UNSET) {
					// not ready yet
					nilCount++
					return 0
				} else {
					// happy path
					okCount++
					return x + y
				}
			} catch (_error) {
				// error path
				errCount++
				return -1
			}
		})

		for (let i = 0; i < 10; i++) {
			toggleState.set(!!(i % 2))
			await wait(10)
			complexComputed.get()
		}

		// Adjusted expectations to be more flexible
		expect(nilCount + okCount + errCount).toBe(10)
		expect(okCount).toBeGreaterThan(0)
		expect(errCount).toBeGreaterThan(0)
	})

	test('should handle signal changes during async computation', async function () {
		const source = state(1)
		let computationCount = 0
		const derived = computed(async abort => {
			computationCount++
			expect(abort?.aborted).toBe(false)
			await wait(100)
			return source.get()
		})

		// Start first computation
		expect(derived.get()).toBe(UNSET)
		expect(computationCount).toBe(1)

		// Change source before first computation completes
		source.set(2)
		await wait(210)
		expect(derived.get()).toBe(2)
		expect(computationCount).toBe(1)
	})

	test('should handle multiple rapid changes during async computation', async function () {
		const source = state(1)
		let computationCount = 0
		const derived = computed(async abort => {
			computationCount++
			expect(abort?.aborted).toBe(false)
			await wait(100)
			return source.get()
		})

		// Start first computation
		expect(derived.get()).toBe(UNSET)
		expect(computationCount).toBe(1)

		// Make multiple rapid changes
		source.set(2)
		source.set(3)
		source.set(4)
		await wait(210)

		// Should have computed twice (initial + final change)
		expect(derived.get()).toBe(4)
		expect(computationCount).toBe(1)
	})

	test('should handle errors in aborted computations', async function () {
		const source = state(1)
		const derived = computed(async () => {
			await wait(100)
			const value = source.get()
			if (value === 2) throw new Error('Intentional error')
			return value
		})

		// Start first computation
		expect(derived.get()).toBe(UNSET)

		// Change to error state before first computation completes
		source.set(2)
		await wait(110)
		expect(() => derived.get()).toThrow('Intentional error')

		// Change to normal state before second computation completes
		source.set(3)
		await wait(100)
		expect(derived.get()).toBe(3)
	})
})
