import type { Tracked } from 'ripple';
import { TRACKED_UPDATED, effect, flushSync, track, trackAsync } from 'ripple';

describe('async suspense', () => {
	it('hides child content during re-suspension when tracked dependency changes', async () => {
		let resolve_fn: (() => void) | null = null;

		function Child({ countTracked }: { countTracked: Tracked<number> }) {
			return <>
				trackAsync(() => {
					countTracked.value;
					return new Promise<void>((resolve) => {
						resolve_fn = resolve;
					});
				});
				<div class="child-content">{'child content'}</div>
				<div class="count">
					{'count is: '}
					{countTracked.value}
				</div>
			</>;
		}

		function App() {
			return <>
				let &[count, countTracked] = track(0);
				try {
					<Child {countTracked} />
				} pending {
					<div class="pending">{'pending...'}</div>
				}
				<button onClick={() => count++}>{'Increment'}</button>
			</>;
		}

		render(App);

		// Initial state: should show pending
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();
		expect(container.innerHTML).toContain('pending...');
		expect(container.innerHTML).not.toContain('child content');

		// Resolve the first promise
		(resolve_fn as () => void)?.();
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// After resolution: should show child content, not pending
		expect(container.innerHTML).toContain('child content');
		expect(container.innerHTML).not.toContain('pending...');

		// Changing count should keep the ui in the same state
		const button = container.querySelector('button');
		button?.click();
		flushSync();
		expect(container.innerHTML).toContain('count is: 1');

		// Wait for microtask to process
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// After count change: should still show child content, not pending
		expect(container.innerHTML).not.toContain('pending...');
		expect(container.innerHTML).toContain('child content');
		expect(container.innerHTML).toContain('count is: 1');
	});

	it('ignores settled promises after the surrounding boundary is destroyed', async () => {
		let resolve_value: ((value: string) => void) | null = null;

		function Child() {
			return <>
				let &[value] = trackAsync(
					() => new Promise<string>((resolve) => {
						resolve_value = resolve;
					}),
				);
				<div class="value">{value}</div>
			</>;
		}

		function App() {
			return <>
				let &[show] = track(true);
				<button onClick={() => (show = false)}>{'Hide'}</button>
				if (show) {
					try {
						<Child />
					} pending {
						<div class="pending">{'loading...'}</div>
					}
				} else {
					<div class="hidden">{'hidden'}</div>
				}
			</>;
		}

		render(App);

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();
		expect(container.innerHTML).toContain('loading...');

		(container.querySelector('button') as HTMLButtonElement).click();
		flushSync();
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.innerHTML).toContain('hidden');

		(resolve_value as (value: string) => void)('late value');
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.innerHTML).toContain('hidden');
		expect(container.innerHTML).not.toContain('late value');
	});

	it('aborts superseded requests with TRACKED_UPDATED without rendering catch', async () => {
		const requests = new Map<
			string,
			{ resolve: (value: string) => void; abortController: AbortController }
		>();

		function createRequest(label: string) {
			const abortController = new AbortController();
			let resolve_value: (value: string) => void = () => {};

			const promise = new Promise<string>((resolve, reject) => {
				resolve_value = resolve;
				abortController.signal.addEventListener('abort', () => {
					reject(abortController.signal.reason);
				});
			});

			requests.set(label, { resolve: resolve_value, abortController });

			return { promise, abortController };
		}

		function App() {
			return <>
				let &[query] = track('a');
				try {
					let &[value] = trackAsync(() => createRequest(query));
					<div class="value">{value}</div>
				} pending {
					<div class="pending">{'loading...'}</div>
				} catch (error) {
					<div class="error">{String(error)}</div>
				}
				<button
					onClick={() => {
						query = query === 'a' ? 'b' : 'c';
					}}
				>
					{'Next'}
				</button>
			</>;
		}

		render(App);

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();
		expect(container.innerHTML).toContain('loading...');

		(requests.get('a') as { resolve: (value: string) => void }).resolve('value-a');
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.innerHTML).toContain('value-a');

		const button = container.querySelector('button') as HTMLButtonElement;
		button.click();
		flushSync();
		button.click();
		flushSync();

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(
			(requests.get('b') as { abortController: AbortController }).abortController.signal.reason,
		).toBe(TRACKED_UPDATED);
		expect(container.innerHTML).not.toContain('class="error"');
		expect(container.innerHTML).toContain('value-a');

		(requests.get('c') as { resolve: (value: string) => void }).resolve('value-c');
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.innerHTML).toContain('value-c');
		expect(container.innerHTML).not.toContain('class="error"');
	});

	it(
		'updates the existing reactive graph when tracked dependency triggers re-fetch in child component',
		async () => {
			let pending_render_count = 0;

			function FilteredList({ queryTracked }: { queryTracked: Tracked<string> }) {
				return <>
					let &[query] = queryTracked;
					let &[items] = trackAsync(
						() => Promise.resolve(
							!query ? ['apple', 'banana', 'cherry'] : ['avocado', 'blueberry', 'citrus'],
						),
					);
					let &[filtered] = track(() => {
						return items.filter((item: string) => item.includes(query));
					});
					<ul>
						for (let item of filtered) {
							<li>{item}</li>
						}
					</ul>
					<pre>{JSON.stringify(items)}</pre>
				</>;
			}

			function App() {
				return <>
					let &[query, queryTracked] = track('');
					try {
						<FilteredList {queryTracked} />
					} pending {
						pending_render_count += 1;
						<p class="pending">{'loading...'}</p>
					}
					<button onClick={() => (query = 'a')}>{'Search'}</button>
				</>;
			}

			render(App);

			// Promise.resolve settles in microtask — wait for it
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();
			expect(pending_render_count).toBe(1);

			// Initial: filtered by '' matches apple, banana (both contain '')
			expect(container.querySelectorAll('li').length).toBe(3);
			expect(container.innerHTML).toContain('apple');
			expect(container.innerHTML).toContain('banana');
			expect(container.innerHTML).toContain('cherry');
			expect(container.querySelectorAll('ul').length).toBe(1);
			expect(container.querySelectorAll('pre').length).toBe(1);
			expect(container.querySelector('pre')!.textContent).toBe('["apple","banana","cherry"]');

			// Change query to 'a' — triggers re-fetch with new items
			(container.querySelector('button') as HTMLButtonElement).click();
			flushSync();
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			// The resolved branch should update in place without appending duplicates.
			expect(pending_render_count).toBe(1);
			expect(container.innerHTML).not.toContain('loading...');
			expect(container.querySelectorAll('ul').length).toBe(1);
			expect(container.querySelectorAll('pre').length).toBe(1);
			// After re-fetch, the branch re-runs with the new data.
			expect(container.innerHTML).toContain('avocado');
			expect(container.innerHTML).toContain('blueberry');
			expect(container.innerHTML).toContain('citrus');
			expect(container.querySelector('pre')!.textContent).toBe('["avocado","blueberry","citrus"]');
			expect(container.innerHTML).not.toContain('apple');
			expect(container.innerHTML).not.toContain('banana');
			expect(container.innerHTML).not.toContain('cherry');
		},
	);

	it('updates direct try-block trackAsync consumers and renders pending only once', async () => {
		let pending_render_count = 0;

		function App() {
			return <>
				let &[query] = track('');
				try {
					let &[items] = trackAsync(
						() => Promise.resolve(
							!query ? ['apple', 'banana', 'cherry'] : ['avocado', 'blueberry', 'citrus'],
						),
					);

					let &[filtered] = track(() => {
						return items.filter((item: string) => item.includes(query));
					});

					<ul>
						for (let item of filtered) {
							<li>{item}</li>
						}
					</ul>

					<pre>{JSON.stringify(items)}</pre>
				} pending {
					pending_render_count += 1;
					<p class="pending">{'loading...'}</p>
				}
				<button onClick={() => (query = 'a')}>{'Search'}</button>
			</>;
		}

		render(App);

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(pending_render_count).toBe(1);
		expect(container.querySelectorAll('li').length).toBe(3);
		expect(container.querySelector('pre')!.textContent).toBe('["apple","banana","cherry"]');

		(container.querySelector('button') as HTMLButtonElement).click();
		flushSync();
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(pending_render_count).toBe(1);
		expect(container.innerHTML).not.toContain('loading...');
		expect(container.querySelectorAll('ul').length).toBe(1);
		expect(container.querySelectorAll('pre').length).toBe(1);
		// After re-fetch, the branch re-runs with the new data.
		expect(container.innerHTML).toContain('avocado');
		expect(container.innerHTML).toContain('blueberry');
		expect(container.innerHTML).toContain('citrus');
		expect(container.innerHTML).not.toContain('apple');
		expect(container.innerHTML).not.toContain('banana');
		expect(container.innerHTML).not.toContain('cherry');
		expect(container.querySelector('pre')!.textContent).toBe('["avocado","blueberry","citrus"]');
	});

	it(
		'defers paused async consumer blocks until chained async requests settle with render only running once',
		async () => {
			let resolve_double: ((value: number) => void) | null = null;
			let resolve_quadruple: ((value: number) => void) | null = null;
			let double_effect_runs = 0;
			let quadruple_effect_runs = 0;
			let times_rendered = 0;
			function Child(&{ count }: { count: number }) {
				return <>
					const &[double]: [number] = trackAsync(() => {
						const result = count * 2;

						return new Promise<number>((resolve) => {
							resolve_double = () => resolve(result);
						});
					});
					const &[quadruple]: [number] = trackAsync(() => {
						const result = double * 2;

						return new Promise<number>((resolve) => {
							resolve_quadruple = () => resolve(result);
						});
					});
					effect(() => {
						double;
						double_effect_runs += 1;
					});
					effect(() => {
						quadruple;
						quadruple_effect_runs += 1;
					});
					// this is to make the times_rendered render together with double
					<div class="double">{double + (++times_rendered - times_rendered)}</div>
					<div class="quadruple">{quadruple}</div>
				</>;
			}

			function App() {
				return <>
					let &[count] = track(2);
					try {
						<Child {count} />
					} pending {
						<div class="pending">{'Loading...'}</div>
					}
					<button onClick={() => count++}>{'Increment'}</button>
				</>;
			}

			render(App);

			const button = container.querySelector('button') as HTMLButtonElement;

			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).toContain('Loading...');
			expect(double_effect_runs).toBe(0);
			expect(quadruple_effect_runs).toBe(0);

			(resolve_double as (value: number) => void)(4);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).toContain('Loading...');
			expect(double_effect_runs).toBe(0);
			expect(quadruple_effect_runs).toBe(0);

			(resolve_quadruple as (value: number) => void)(8);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).not.toContain('Loading...');
			expect(container.querySelector('.double')!.textContent).toBe('4');
			expect(container.querySelector('.quadruple')!.textContent).toBe('8');
			expect(double_effect_runs).toBe(1);
			expect(quadruple_effect_runs).toBe(1);
			expect(times_rendered).toBe(1);

			button.click();
			flushSync();

			(resolve_double as (value: number) => void)(6);
			await new Promise((resolve) => setTimeout(resolve, 0));
			(resolve_quadruple as (value: number) => void)(12);
			await new Promise((resolve) => setTimeout(resolve, 0));
			expect(container.querySelector('.double')!.textContent).toBe('6');
			expect(container.querySelector('.quadruple')!.textContent).toBe('12');
			expect(times_rendered).toBe(2);
		},
	);

	it(
		'chained async requests settle with render only running once when multiple consecutive requests are fired off',
		async () => {
			let resolve_double: ((value: number) => void) | null = null;
			let resolve_quadruple: ((value: number) => void) | null = null;
			let double_effect_runs = 0;
			let quadruple_effect_runs = 0;
			let times_rendered = 0;
			function Child(&{ count }: { count: number }) {
				return <>
					const &[double]: [number] = trackAsync(() => {
						const result = count * 2;

						return new Promise<number>((resolve) => {
							resolve_double = () => resolve(result);
						});
					});
					const &[quadruple]: [number] = trackAsync(() => {
						const result = double * 2;

						return new Promise<number>((resolve) => {
							resolve_quadruple = () => resolve(result);
						});
					});
					effect(() => {
						double;
						double_effect_runs += 1;
					});
					effect(() => {
						quadruple;
						quadruple_effect_runs += 1;
					});
					// this is to make the times_rendered render together with double
					<div class="double">{double + (++times_rendered - times_rendered)}</div>
					<div class="quadruple">{quadruple}</div>
				</>;
			}

			function App() {
				return <>
					let &[count] = track(2);
					try {
						<Child {count} />
					} pending {
						<div class="pending">{'Loading...'}</div>
					}
					<button onClick={() => count++}>{'Increment'}</button>
				</>;
			}

			render(App);

			const button = container.querySelector('button') as HTMLButtonElement;

			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).toContain('Loading...');
			expect(double_effect_runs).toBe(0);
			expect(quadruple_effect_runs).toBe(0);

			(resolve_double as (value: number) => void)(4);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).toContain('Loading...');
			expect(double_effect_runs).toBe(0);
			expect(quadruple_effect_runs).toBe(0);

			(resolve_quadruple as (value: number) => void)(8);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).not.toContain('Loading...');
			expect(container.querySelector('.double')!.textContent).toBe('4');
			expect(container.querySelector('.quadruple')!.textContent).toBe('8');
			expect(double_effect_runs).toBe(1);
			expect(quadruple_effect_runs).toBe(1);
			expect(times_rendered).toBe(1);

			button.click();
			flushSync();
			button.click();
			flushSync();
			button.click();
			flushSync();

			(resolve_double as (value: number) => void)(10);
			await new Promise((resolve) => setTimeout(resolve, 0));
			(resolve_quadruple as (value: number) => void)(12);
			await new Promise((resolve) => setTimeout(resolve, 0));
			expect(container.querySelector('.double')!.textContent).toBe('10');
			expect(container.querySelector('.quadruple')!.textContent).toBe('20');
			expect(times_rendered).toBe(2);
		},
	);

	it('resolves chained async values without explicit flushSync', async () => {
		let times_rendered = 0;
		function Child(&{ count }: { count: number }) {
			return <>
				const &[double]: [number] = trackAsync(() => {
					const result = count * 2;

					return new Promise<number>((resolve) => {
						setTimeout(() => resolve(result), 0);
					});
				});
				const &[quadruple]: [number] = trackAsync(() => {
					const result = double * 2;

					return new Promise<number>((resolve) => {
						setTimeout(() => resolve(result), 0);
					});
				});
				// this is to make the times_rendered render together with double
				<div class="double">{double + (++times_rendered - times_rendered)}</div>
				<div class="quadruple">{quadruple}</div>
			</>;
		}

		function App() {
			return <>
				let &[count] = track(0);
				<button onClick={() => count++}>{count}</button>
				try {
					<Child {count} />
				} pending {
					<div class="pending">{'Loading...'}</div>
				}
			</>;
		}

		render(App);

		for (let i = 0; i < 50; i++) {
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();
			if (
				!container.innerHTML.includes('Loading...') &&
					container.querySelector('.double')?.textContent === '0' &&
					container.querySelector('.quadruple')?.textContent === '0'
			) {
				break;
			}
		}

		expect(container.innerHTML).not.toContain('Loading...');
		expect(container.querySelector('.double')!.textContent).toBe('0');
		expect(container.querySelector('.quadruple')!.textContent).toBe('0');
		expect(times_rendered).toBe(1);
	});

	it(
		'registers async derived under multiple try/pending boundaries across components',
		async () => {
			let resolve_fn: ((value: string[]) => void) | null = null;

			function Child({ itemsTracked }: { itemsTracked: Tracked<string[]> }) {
				return <>
					try {
						<div class="child-content">
							{'child: '}
							{itemsTracked.value.join(', ')}
						</div>
					} pending {
						<div class="child-pending">{'child loading...'}</div>
					}
				</>;
			}

			function App() {
				return <>
					let &[query] = track('initial');
					try {
						let &[items, itemsTracked] = trackAsync(() => {
							const q = query;
							return new Promise<string[]>((resolve) => {
								resolve_fn = resolve;
							});
						});

						<div class="parent-content">
							{'parent: '}
							{items.join(', ')}
						</div>
						<Child {itemsTracked} />
					} pending {
						<div class="parent-pending">{'parent loading...'}</div>
					}
					<button onClick={() => (query = 'next')}>{'Change'}</button>
				</>;
			}

			render(App);

			// Initial: parent pending, child not yet rendered
			await new Promise((resolve) => setTimeout(resolve, 0));
			expect(container.innerHTML).toContain('parent loading...');
			expect(container.innerHTML).not.toContain('parent-content');
			expect(container.innerHTML).not.toContain('child-content');

			// Resolve first request — both parent and child should show content,
			// child reading the derived registers its own try boundary on the same derived
			(resolve_fn as (value: string[]) => void)(['apple', 'banana']);
			await new Promise((resolve) => setTimeout(resolve, 0));

			expect(container.innerHTML).toContain('parent: apple, banana');
			expect(container.innerHTML).toContain('child: apple, banana');
			expect(container.innerHTML).not.toContain('parent loading...');
			expect(container.innerHTML).not.toContain('child loading...');

			// Trigger re-fetch — derived now has entries for both parent and child boundaries
			const button = container.querySelector('button') as HTMLButtonElement;
			button.click();
			flushSync();
			await new Promise((resolve) => setTimeout(resolve, 0));

			// After has_resolved, re-fetch keeps resolved branch visible (no re-suspension)
			expect(container.innerHTML).not.toContain('parent loading...');
			expect(container.innerHTML).not.toContain('child loading...');

			// Resolve second request — both should update in place
			(resolve_fn as (value: string[]) => void)(['cherry', 'date']);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.innerHTML).toContain('parent: cherry, date');
			expect(container.innerHTML).toContain('child: cherry, date');
			expect(container.innerHTML).not.toContain('parent loading...');
			expect(container.innerHTML).not.toContain('child loading...');
		},
	);

	it('uses the root pending boundary when trackAsync has no local try/pending boundary', () => {
		function App() {
			return <>
				let &[value] = trackAsync(() => Promise.resolve('test'));
				<div>{value}</div>
			</>;
		}

		expect(() => {
			render(App);
		}).not.toThrow();
	});
});
