import type { Tracked } from 'ripple';
import {
	bindValue,
	effect,
	flushSync,
	track,
	trackAsync,
	peek,
	UNINITIALIZED,
	SUSPENSE_REJECTED,
	SUSPENSE_PENDING,
} from 'ripple';

describe('try block with catch and pending', () => {
	it('renders nothing for an empty pending fallback before resolving', async () => {
		let resolve_value: (value: string) => void = () => {};
		const promise = new Promise<string>((resolve) => {
			resolve_value = resolve;
		});

		function App() {
			return <>
				<span class="before">{'before'}</span>
				try {
					let &[data] = trackAsync(() => promise);
					<p class="resolved">{data}</p>
				} pending {}
				<span class="after">{'after'}</span>
			</>;
		}

		render(App);

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

		expect(container.querySelector('.before')?.textContent).toBe('before');
		expect(container.querySelector('.after')?.textContent).toBe('after');
		expect(container.querySelector('.resolved')).toBeNull();
		expect(container.innerHTML).not.toContain('loading');

		resolve_value('resolved value');
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.resolved')?.textContent).toBe('resolved value');
		expect(container.querySelector('.before')?.textContent).toBe('before');
		expect(container.querySelector('.after')?.textContent).toBe('after');
	});

	it('catch block works when a child throws synchronously', async () => {
		function App() {
			return <>
				try {
					<ThrowingChild />
				} pending {
					<p>{'loading...'}</p>
				} catch (err) {
					<p>{'caught error'}</p>
				}
			</>;
		}

		function ThrowingChild() {
			return <>
				throw new Error('sync error');
			</>;
		}

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

		expect(container.innerHTML).toContain('caught error');
		expect(container.innerHTML).not.toContain('loading...');
	});

	it('catch block works when component throws after trackAsync with pending block', async () => {
		function App() {
			return <>
				try {
					<ThrowingAfterAsync />
				} pending {
					<p>{'loading...'}</p>
				} catch (err) {
					<p>{'caught error'}</p>
				}
			</>;
		}

		function ThrowingAfterAsync() {
			return <>
				let &[data] = trackAsync(() => Promise.resolve('hello'));
				throw new Error('error after await');
				<p>{data}</p>
			</>;
		}

		render(App);

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

		expect(container.innerHTML).toContain('caught error');
		expect(container.innerHTML).not.toContain('loading...');
	});

	it('catch block works when trackAsync rejects', async () => {
		function App() {
			return <>
				try {
					let &[data] = trackAsync(() => Promise.reject(new Error('rejected')));
					<p>{data}</p>
				} pending {
					<p>{'loading...'}</p>
				} catch (err) {
					<p>{'caught rejection'}</p>
				}
			</>;
		}

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

		expect(container.innerHTML).toContain('caught rejection');
		expect(container.innerHTML).not.toContain('loading...');
	});

	it('catch replaces the retained resolved branch when a subsequent request rejects', async () => {
		const requests = new Map<
			number,
			{ resolve: (value: string) => void; reject: (error: Error) => void }
		>();

		function createRequest(version: number) {
			let resolve_value: (value: string) => void = () => {};
			let reject_value: (error: Error) => void = () => {};

			const promise = new Promise<string>((resolve, reject) => {
				resolve_value = resolve;
				reject_value = reject;
			});

			requests.set(version, { resolve: resolve_value, reject: reject_value });

			return promise;
		}

		function App() {
			return <>
				let &[version] = track(0);
				try {
					let &[data] = trackAsync(() => createRequest(version));
					<p class="resolved">{data}</p>
				} pending {
					<p class="pending">{'loading...'}</p>
				} catch (err) {
					<p class="caught">{(err as Error).message}</p>
				}
				<button class="retry" onClick={() => version++}>{'Retry'}</button>
			</>;
		}

		render(App);

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();
		expect(container.querySelector('.pending')?.textContent).toBe('loading...');

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

		expect(container.querySelector('.resolved')?.textContent).toBe('resolved value');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.caught')).toBeNull();

		(container.querySelector('.retry') as HTMLButtonElement).click();
		flushSync();

		expect(container.querySelector('.resolved')?.textContent).toBe('resolved value');
		expect(container.querySelector('.pending')).toBeNull();

		(requests.get(1) as { reject: (error: Error) => void }).reject(new Error('failed retry'));
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.caught')?.textContent).toBe('failed retry');
		expect(container.querySelector('.resolved')).toBeNull();
		expect(container.querySelector('.pending')).toBeNull();
	});

	it('retrying from catch shows pending before resolving back to the resolved branch', async () => {
		const requests = new Map<
			number,
			{ resolve: (value: string) => void; reject: (error: Error) => void }
		>();

		function createRequest(version: number) {
			let resolve_value: (value: string) => void = () => {};
			let reject_value: (error: Error) => void = () => {};

			const promise = new Promise<string>((resolve, reject) => {
				resolve_value = resolve;
				reject_value = reject;
			});

			requests.set(version, { resolve: resolve_value, reject: reject_value });

			return promise;
		}

		function App() {
			return <>
				let &[version] = track(0);
				try {
					let &[data] = trackAsync(() => {
						version;
						return createRequest(version);
					});
					<p class="resolved">{data}</p>
				} pending {
					<p class="pending">{'loading...'}</p>
				} catch (err, reset) {
					<p class="caught">{(err as Error).message}</p>
					<button
						class="retry"
						onClick={() => {
							version++;
							reset();
						}}
					>
						{'Retry'}
					</button>
				}
			</>;
		}

		render(App);

		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();
		expect(container.querySelector('.pending')?.textContent).toBe('loading...');

		(requests.get(0) as { reject: (error: Error) => void }).reject(new Error('initial failure'));
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.caught')?.textContent).toBe('initial failure');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();

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

		expect(container.querySelector('.pending')?.textContent).toBe('loading...');
		expect(container.querySelector('.caught')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();

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

		expect(container.querySelector('.resolved')?.textContent).toBe('recovered');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.caught')).toBeNull();
	});
});

describe('try block', () => {
	it(
		'does not compile ref binds as async callbacks inside try/pending trackAsync branches',
		async () => {
			function App() {
				return <>
					let &[value, valueTracked] = track(1);
					try {
						let &[loaded] = trackAsync(() => Promise.resolve(value + 1));
						<input type="number" ref={bindValue(valueTracked)} />
						<span>{loaded}</span>
					} pending {
						<p>{'loading...'}</p>
					}
				</>;
			}

			render(App);

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

			const input = container.querySelector('input') as HTMLInputElement | null;
			expect(input?.value).toBe('1');
			expect(container.innerHTML).toContain('<span>2</span>');
			expect(container.innerHTML).not.toContain('loading...');
		},
	);

	it('does not crash when trackAsync is used to render a list inside try/pending', async () => {
		function App() {
			return <>
				try {
					<AsyncChild />
				} pending {
					<p>{'loading...'}</p>
				}
			</>;
		}

		function AsyncChild() {
			return <>
				let &[data] = trackAsync(() => Promise.resolve(['a', 'b', 'c']));
				<ul>
					for (let item of data) {
						<li>{item}</li>
					}
				</ul>
			</>;
		}

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

		const items = container.querySelectorAll('li');
		expect(items.length).toBe(3);
		expect(items[0].textContent).toBe('a');
		expect(items[1].textContent).toBe('b');
		expect(items[2].textContent).toBe('c');
	});

	it(
		'does not crash when async component with tracked state is used inside try/pending',
		async () => {
			function App() {
				return <>
					let &[query, queryTracked] = track('');
					try {
						<FilteredList {queryTracked} />
					} pending {
						<p>{'loading...'}</p>
					}
				</>;
			}

			function FilteredList({ queryTracked }: { queryTracked: Tracked<string> }) {
				return <>
					let &[items] = trackAsync(() => Promise.resolve(['apple', 'banana', 'cherry']));
					let &[filtered] = track(
						() => items.filter((item: string) => item.includes(queryTracked.value)),
					);
					<ul>
						for (let item of filtered) {
							<li>{item}</li>
						}
					</ul>
				</>;
			}

			render(App);

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

			const listItems = container.querySelectorAll('li');
			expect(listItems.length).toBe(3);
		},
	);

	it('if test condition stays synchronous once trackAsync has resolved', async () => {
		function App() {
			return <>
				try {
					let &[items] = trackAsync(() => Promise.resolve(['apple', 'banana', 'cherry']));

					if (items.includes('not-in-list')) {
						<p>{'not-in-list is in the list!'}</p>
					} else {
						<p>{'not-in-list is not in the list.'}</p>
					}
				} pending {
					<p>{'loading...'}</p>
				}
			</>;
		}

		render(App);

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

		expect(container.innerHTML).toContain('not-in-list is not in the list.');
	});

	it('destroying try block while in pending state cleans up pending branch', async () => {
		let pending_effect_teardown_count = 0;

		function PendingChild() {
			return <>
				effect(() => {
					return () => {
						pending_effect_teardown_count++;
					};
				});
				<p class="pending">{'loading...'}</p>
			</>;
		}

		function App() {
			return <>
				let &[show] = track(true);
				if (show) {
					try {
						let &[data] = trackAsync(() => new Promise(() => {}));
						<p class="resolved">{data}</p>
					} pending {
						<PendingChild />
					}
				}
				<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
			</>;
		}

		render(App);

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

		expect(container.querySelector('.pending')?.textContent).toBe('loading...');
		expect(pending_effect_teardown_count).toBe(0);

		// Toggle if condition to false — should destroy try block and pending branch
		(container.querySelector('.toggle') as HTMLButtonElement).click();
		flushSync();

		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();
		// Effect teardown in the pending branch should have run
		expect(pending_effect_teardown_count).toBe(1);
	});

	it('destroying try block while in resolved state cleans up resolved branch', async () => {
		let resolved_effect_teardown_count = 0;

		function ResolvedChild(&{ data }: { data: string }) {
			return <>
				effect(() => {
					return () => {
						resolved_effect_teardown_count++;
					};
				});
				<p class="resolved">{data}</p>
			</>;
		}

		function App() {
			return <>
				let &[show] = track(true);
				if (show) {
					try {
						let &[data] = trackAsync(() => Promise.resolve('hello'));
						<ResolvedChild {data} />
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<p class="caught">{(err as Error).message}</p>
					}
				}
				<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
			</>;
		}

		render(App);

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

		expect(container.querySelector('.resolved')?.textContent).toBe('hello');
		expect(resolved_effect_teardown_count).toBe(0);

		// Toggle if condition to false — should destroy try block and resolved branch
		(container.querySelector('.toggle') as HTMLButtonElement).click();
		flushSync();

		expect(container.querySelector('.resolved')).toBeNull();
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.caught')).toBeNull();
		// Effect teardown in the resolved branch should have run
		expect(resolved_effect_teardown_count).toBe(1);
	});

	it('destroying try block while in catch state cleans up catch branch', async () => {
		let catch_effect_teardown_count = 0;

		function CatchChild({ error }: { error: Error }) {
			return <>
				effect(() => {
					return () => {
						catch_effect_teardown_count++;
					};
				});
				<p class="caught">{error.message}</p>
			</>;
		}

		function App() {
			return <>
				let &[show] = track(true);
				if (show) {
					try {
						let &[data] = trackAsync(() => Promise.reject(new Error('fail')));
						<p class="resolved">{data}</p>
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<CatchChild error={err as Error} />
					}
				}
				<button class="toggle" onClick={() => (show = !show)}>{'toggle'}</button>
			</>;
		}

		render(App);

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

		expect(container.querySelector('.caught')?.textContent).toBe('fail');
		expect(catch_effect_teardown_count).toBe(0);

		// Toggle if condition to false — should destroy try block and catch branch
		(container.querySelector('.toggle') as HTMLButtonElement).click();
		flushSync();

		expect(container.querySelector('.caught')).toBeNull();
		// Effect teardown in the catch branch should have run
		expect(catch_effect_teardown_count).toBe(1);
	});

	it('pending block throwing renders catch block from the same try block', async () => {
		function ThrowingPending() {
			return <>
				throw new Error('pending exploded');
				<p>{'should not render'}</p>
			</>;
		}

		function App() {
			return <>
				try {
					let &[data] = trackAsync(() => new Promise(() => {}));
					<p class="resolved">{data}</p>
				} pending {
					<ThrowingPending />
				} catch (err) {
					<p class="caught">{(err as Error).message}</p>
				}
			</>;
		}

		render(App);

		// Wait for pending microtask to fire and render pending block
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// Pending block threw, so catch should be rendered
		expect(container.querySelector('.caught')?.textContent).toBe('pending exploded');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();
	});

	it('pending block throwing without catch bubbles to parent try/catch', async () => {
		function ThrowingPending() {
			return <>
				throw new Error('pending exploded');
				<p>{'should not render'}</p>
			</>;
		}

		function App() {
			return <>
				try {
					<Inner />
				} catch (err) {
					<p class="parent-caught">{(err as Error).message}</p>
				}
			</>;
		}

		function Inner() {
			return <>
				try {
					let &[data] = trackAsync(() => new Promise(() => {}));
					<p class="resolved">{data}</p>
				} pending {
					<ThrowingPending />
				}
			</>;
		}

		render(App);

		// Wait for pending microtask to fire and render pending block
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// Inner try has no catch, error should bubble to parent's catch
		expect(container.querySelector('.parent-caught')?.textContent).toBe('pending exploded');
		expect(container.querySelector('.resolved')).toBeNull();
	});

	it('chained trackAsync keeps pending block until all deriveds resolve', async () => {
		let first_resolve: (value: string) => void;
		let second_resolve: (value: number) => void;

		const first_promise = new Promise<string>((resolve) => {
			first_resolve = resolve;
		});

		function App() {
			return <>
				try {
					let &[name] = trackAsync(() => first_promise);
					let &[length] = trackAsync(() => {
						// Read name synchronously — throws ASYNC_DERIVED_READ_THROWN
						// while name is still pending, triggering the deferred mechanism.
						const n = name;
						return new Promise<number>((resolve) => {
							second_resolve = resolve;
						}).then((val) => n.length + val);
					});
					<p class="resolved">
						{name}
						{' has length '}
						{length}
					</p>
				} pending {
					<p class="pending">{'loading...'}</p>
				}
			</>;
		}

		render(App);

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

		// Both are pending — pending block should show
		expect(container.querySelector('.pending')?.textContent).toBe('loading...');
		expect(container.querySelector('.resolved')).toBeNull();

		// Resolve the first trackAsync
		first_resolve!('hello');
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// First resolved, but second still pending — pending block must stay
		expect(container.querySelector('.pending')?.textContent).toBe('loading...');
		expect(container.querySelector('.resolved')).toBeNull();

		// Resolve the second trackAsync
		second_resolve!(100);
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		// Both resolved — pending removed, resolved content shown with both values
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')?.textContent).toBe('hello has length 105');
	});

	it(
		'chained trackAsync does not recreate pending block when intermediate derived resolves',
		async () => {
			let first_resolve: (value: string) => void;
			let second_resolve: (value: number) => void;
			let pending_render_count = 0;

			const first_promise = new Promise<string>((resolve) => {
				first_resolve = resolve;
			});

			function PendingTracker() {
				return <>
					pending_render_count++;
					<p class="pending">{'loading...'}</p>
				</>;
			}

			function App() {
				return <>
					try {
						let &[name] = trackAsync(() => first_promise);
						let &[length] = trackAsync(() => {
							const n = name;
							return new Promise<number>((resolve) => {
								second_resolve = resolve;
							}).then((val) => n.length + val);
						});
						<p class="resolved">
							{name}
							{' has length '}
							{length}
						</p>
					} pending {
						<PendingTracker />
					}
				</>;
			}

			render(App);

			// Wait for pending microtask to fire and render pending block
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');
			expect(pending_render_count).toBe(1);

			// Resolve the first — second starts its own async request
			// Pending block should NOT be torn down and recreated
			first_resolve!('hello');
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');
			expect(pending_render_count).toBe(1); // Still 1 — no flicker

			// Resolve the second — pending removed, resolved shown
			second_resolve!(100);
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.resolved')?.textContent).toBe('hello has length 105');
			expect(pending_render_count).toBe(1); // Never recreated
		},
	);

	it(
		'chained trackAsync: first rejects, catch renders and stays after second resolves',
		async () => {
			let first_reject: (error: Error) => void;
			let second_resolve: (value: number) => void;

			const first_promise = new Promise<string>((_, reject) => {
				first_reject = reject;
			});

			function App() {
				return <>
					try {
						let &[name] = trackAsync(() => first_promise);
						let &[length] = trackAsync(() => {
							const n = name;
							return new Promise<number>((resolve) => {
								second_resolve = resolve;
							}).then((val) => n.length + val);
						});
						<p class="resolved">
							{name}
							{' has length '}
							{length}
						</p>
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<p class="caught">{(err as Error).message}</p>
					}
				</>;
			}

			render(App);

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

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.caught')).toBeNull();

			// Reject the first trackAsync
			first_reject!(new Error('first failed'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			// Catch block should render
			expect(container.querySelector('.caught')?.textContent).toBe('first failed');
			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.resolved')).toBeNull();

			// Resolve the second trackAsync — catch block should stay
			expect(second_resolve!).toBeUndefined();
		},
	);

	it(
		'chained trackAsync: first succeeds, second rejects, pending stays then catch renders',
		async () => {
			let first_resolve: (value: string) => void;
			let second_reject: (error: Error) => void;

			const first_promise = new Promise<string>((resolve) => {
				first_resolve = resolve;
			});

			function App() {
				return <>
					try {
						let &[name] = trackAsync(() => first_promise);
						let &[length] = trackAsync(() => {
							const n = name;
							return new Promise<number>((_, reject) => {
								second_reject = reject;
							});
						});
						<p class="resolved">
							{name}
							{' has length '}
							{length}
						</p>
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<p class="caught">{(err as Error).message}</p>
					}
				</>;
			}

			render(App);

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

			// Both pending — pending block shows
			expect(container.querySelector('.pending')?.textContent).toBe('loading...');
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.caught')).toBeNull();

			// Resolve the first — second still pending, pending block stays
			first_resolve!('hello');
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.caught')).toBeNull();

			// Reject the second — switch to catch
			second_reject!(new Error('second failed'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.caught')?.textContent).toBe('second failed');
			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.resolved')).toBeNull();
		},
	);

	it('resolved try block without catch bubbles error to parent try/catch', async () => {
		function ThrowingChild() {
			return <>
				let &[data] = trackAsync(() => Promise.reject(new Error('async failed')));
				<p class="resolved">{data}</p>
			</>;
		}

		function App() {
			return <>
				try {
					<Inner />
				} catch (err) {
					<p class="parent-caught">{(err as Error).message}</p>
				}
			</>;
		}

		function Inner() {
			return <>
				try {
					<ThrowingChild />
				} pending {
					<p class="pending">{'loading...'}</p>
				}
			</>;
		}

		render(App);

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

		// Inner try has no catch, rejection should bubble to parent's catch
		expect(container.querySelector('.parent-caught')?.textContent).toBe('async failed');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();
	});

	it(
		'rejection sets derived value to SUSPENSE_REJECTED and promise is properly handled',
		async () => {
			let reject_fn: (error: Error) => void;
			let promise_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;

			const user_promise = new Promise<string>((_, reject) => {
				reject_fn = reject;
			});
			user_promise.then(
				(v) => {
					promise_settled = { type: 'resolved', value: v };
				},
				(e) => {
					promise_settled = { type: 'rejected', value: e };
				},
			);

			let data: any;

			function App() {
				return <>
					try {
						data = trackAsync(() => user_promise);
						<p class="resolved">{data.value}</p>
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<p class="caught">{(err as Error).message}</p>
					}
				</>;
			}

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

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');

			reject_fn!(new Error('test rejection'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.caught')?.textContent).toBe('test rejection');
			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.resolved')).toBeNull();

			// The user promise was rejected and handled by the runtime
			expect(promise_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
			expect((promise_settled as any).value.message).toBe('test rejection');
			expect(peek(data)).toEqual(SUSPENSE_REJECTED);
		},
	);

	it(
		'chained trackAsync: first rejects, dependent deferred is rejected and cleaned up',
		async () => {
			let first_reject: (error: Error) => void;
			let first_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;

			const first_promise = new Promise<string>((_, reject) => {
				first_reject = reject;
			});
			first_promise.then(
				(v) => {
					first_settled = { type: 'resolved', value: v };
				},
				(e) => {
					first_settled = { type: 'rejected', value: e };
				},
			);

			let name: any;
			let length: any;

			function App() {
				return <>
					try {
						name = trackAsync(() => first_promise);
						length = trackAsync(() => {
							const n = name.value;
							return new Promise<number>((resolve) => {
								resolve(n.length);
							});
						});
						<p class="resolved">
							{name.value}
							{' has length '}
							{length.value}
						</p>
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err) {
						<p class="caught">{(err as Error).message}</p>
					}
				</>;
			}

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

			expect(container.querySelector('.pending')?.textContent).toBe('loading...');

			// Reject the first — should route to catch and clean up second's deferred
			first_reject!(new Error('first failed'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.caught')?.textContent).toBe('first failed');
			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.resolved')).toBeNull();
			expect(peek(name)).toEqual(SUSPENSE_REJECTED);

			// First promise was rejected and handled
			expect(first_settled).toEqual({ type: 'rejected', value: expect.any(Error) });

			// Verify stability — catch stays, no further state changes
			await new Promise((resolve) => setTimeout(resolve, 50));
			flushSync();
			expect(container.querySelector('.caught')?.textContent).toBe('first failed');
			// length never has a chance to run because name access throws and exits the block
			expect(peek(length)).toEqual(SUSPENSE_REJECTED);
		},
	);

	it('deep chained trackAsync: A rejects, B and C deferreds are cleaned up', async () => {
		let a_reject: (error: Error) => void;
		let a_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;

		const a_promise = new Promise<string>((_, reject) => {
			a_reject = reject;
		});
		a_promise.then(
			(v) => {
				a_settled = { type: 'resolved', value: v };
			},
			(e) => {
				a_settled = { type: 'rejected', value: e };
			},
		);

		let a: any;
		let b: any;
		let c: any;
		function App() {
			return <>
				try {
					a = trackAsync(() => a_promise);
					b = trackAsync(() => {
						const val = a.value;
						return Promise.resolve(val.toUpperCase());
					});
					c = trackAsync(() => {
						const val = b.value;
						return Promise.resolve(val.length);
					});
					<p class="resolved">
						{a.value}
						{' → '}
						{b.value}
						{' → '}
						{c.value}
					</p>
				} pending {
					<p class="pending">{'loading...'}</p>
				} catch (err) {
					<p class="caught">{(err as Error).message}</p>
				}
			</>;
		}

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

		expect(container.querySelector('.pending')?.textContent).toBe('loading...');

		// Reject A — should cascade to catch, B and C cleaned up
		a_reject!(new Error('chain failed'));
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.caught')?.textContent).toBe('chain failed');
		expect(container.querySelector('.pending')).toBeNull();
		expect(container.querySelector('.resolved')).toBeNull();

		// A promise was rejected and handled
		expect(a_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
		expect(peek(a)).toEqual(SUSPENSE_REJECTED);
		expect(peek(b)).toEqual(SUSPENSE_REJECTED);
		expect(peek(c)).toEqual(SUSPENSE_REJECTED);

		// Verify stability — no further state changes from B/C deferreds settling
		await new Promise((resolve) => setTimeout(resolve, 50));
		flushSync();
		expect(container.querySelector('.caught')?.textContent).toBe('chain failed');
	});

	it('multiple independent trackAsyncs, one rejects, catch shows first error', async () => {
		let first_reject: (error: Error) => void;
		let second_resolve: (value: number) => void;
		let first_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;
		let second_settled: { type: 'resolved' | 'rejected'; value: any } | null = null;

		const first_promise = new Promise<string>((_, reject) => {
			first_reject = reject;
		});
		first_promise.then(
			(v) => {
				first_settled = { type: 'resolved', value: v };
			},
			(e) => {
				first_settled = { type: 'rejected', value: e };
			},
		);

		const second_promise = new Promise<number>((resolve) => {
			second_resolve = resolve;
		});
		second_promise.then(
			(v) => {
				second_settled = { type: 'resolved', value: v };
			},
			(e) => {
				second_settled = { type: 'rejected', value: e };
			},
		);

		let name: any;
		let count: any;
		function App() {
			return <>
				try {
					name = trackAsync(() => first_promise);
					count = trackAsync(() => second_promise);
					<p class="resolved">
						{name.value}
						{' count: '}
						{count.value}
					</p>
				} pending {
					<p class="pending">{'loading...'}</p>
				} catch (err) {
					<p class="caught">{(err as Error).message}</p>
				}
			</>;
		}

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

		expect(container.querySelector('.pending')?.textContent).toBe('loading...');
		expect(peek(name)).toEqual(SUSPENSE_PENDING);
		expect(peek(count)).toEqual(SUSPENSE_PENDING);

		// Reject the first — catch should render
		first_reject!(new Error('name failed'));
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.caught')?.textContent).toBe('name failed');
		expect(container.querySelector('.pending')).toBeNull();

		// First promise was rejected
		expect(first_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
		expect(peek(name)).toEqual(SUSPENSE_REJECTED);
		expect(peek(count)).toEqual(SUSPENSE_PENDING);

		// Resolve the second — catch should stay stable
		second_resolve!(42);
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(container.querySelector('.caught')?.textContent).toBe('name failed');
		expect(container.querySelector('.resolved')).toBeNull();

		// Second promise was resolved (by the test), but the boundary already caught
		expect(second_settled).toEqual({ type: 'resolved', value: 42 });
		expect(peek(name)).toEqual(SUSPENSE_REJECTED);
		expect(peek(count)).toEqual(42);
	});
});

describe('sync error while async deriveds are pending', () => {
	it(
		'catch stays stable when sync error fires while promise is pending, then promise resolves',
		async () => {
			let resolve_fn: ((v: string) => void) | undefined;
			let user_promise = new Promise<string>((resolve) => {
				resolve_fn = resolve;
			});
			let promise_settled: { type: string; value: any } | null = null;
			user_promise.then(
				(v) => (promise_settled = { type: 'resolved', value: v }),
				(e) => (promise_settled = { type: 'rejected', value: e }),
			);

			function App() {
				return <>
					try {
						<AsyncChild />
						<ThrowingChild />
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err: Error) {
						<p class="caught">{err.message}</p>
					}
				</>;
			}

			let data: any;
			function AsyncChild() {
				return <>
					data = trackAsync(() => user_promise);
					<p class="async-resolved">{data.value}</p>
				</>;
			}

			function ThrowingChild() {
				return <>
					throw new Error('sync boom');
				</>;
			}

			render(App);
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// The sync error from ThrowingChild should trigger catch
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.pending')).toBeNull();
			expect(container.querySelector('.async-resolved')).toBeNull();

			expect(peek(data)).toEqual(SUSPENSE_PENDING);

			// Now resolve the promise that was in-flight
			resolve_fn!('hello');
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// Catch should stay stable — boundary should NOT switch to resolved
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.async-resolved')).toBeNull();
			expect(promise_settled).toEqual({ type: 'resolved', value: 'hello' });

			expect(peek(data)).toEqual('hello');

			// Verify stability after all microtasks drain
			await new Promise((r) => setTimeout(r, 50));
			flushSync();
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.async-resolved')).toBeNull();
		},
	);

	it(
		'catch stays stable when sync error fires while promise is pending, then promise rejects',
		async () => {
			let reject_fn: ((e: any) => void) | undefined;
			let user_promise = new Promise<string>((_, reject) => {
				reject_fn = reject;
			});
			let promise_settled: { type: string; value: any } | null = null;
			user_promise.then(
				(v) => (promise_settled = { type: 'resolved', value: v }),
				(e) => (promise_settled = { type: 'rejected', value: e }),
			);

			function App() {
				return <>
					try {
						<AsyncChild />
						<ThrowingChild />
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err: Error) {
						<p class="caught">{err.message}</p>
					}
				</>;
			}

			let data: any;
			function AsyncChild() {
				return <>
					data = trackAsync(() => user_promise);
					<p class="async-resolved">{data.value}</p>
				</>;
			}

			function ThrowingChild() {
				return <>
					throw new Error('sync boom');
				</>;
			}

			render(App);
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// Catch should be showing from sync error
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');

			expect(peek(data)).toEqual(SUSPENSE_PENDING);

			// Now reject the in-flight promise
			reject_fn!(new Error('async failure'));
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// Catch should stay stable with the original sync error — no double-catch
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(promise_settled).toEqual({ type: 'rejected', value: expect.any(Error) });

			expect(peek(data)).toEqual(SUSPENSE_REJECTED);

			// Verify stability after all microtasks drain
			await new Promise((r) => setTimeout(r, 0));
			flushSync();
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.async-resolved')).toBeNull();
		},
	);

	it(
		'catch stays stable when sync error fires with multiple pending trackAsyncs, all later resolve',
		async () => {
			let resolve_a: ((v: string) => void) | undefined;
			let resolve_b: ((v: number) => void) | undefined;
			let promise_a = new Promise<string>((resolve) => {
				resolve_a = resolve;
			});
			let promise_b = new Promise<number>((resolve) => {
				resolve_b = resolve;
			});

			let settled_a: { type: string; value: any } | null = null;
			let settled_b: { type: string; value: any } | null = null;
			promise_a.then(
				(v) => (settled_a = { type: 'resolved', value: v }),
				(e) => (settled_a = { type: 'rejected', value: e }),
			);
			promise_b.then(
				(v) => (settled_b = { type: 'resolved', value: v }),
				(e) => (settled_b = { type: 'rejected', value: e }),
			);

			function App() {
				return <>
					try {
						<ChildA />
						<ChildB />
						<ThrowingChild />
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err: Error) {
						<p class="caught">{err.message}</p>
					}
				</>;
			}

			let dataA: any;
			function ChildA() {
				return <>
					dataA = trackAsync(() => promise_a);
					<p class="a">{dataA.value}</p>
				</>;
			}

			let dataB: any;
			function ChildB() {
				return <>
					dataB = trackAsync(() => promise_b);
					<p class="b">{dataB.value}</p>
				</>;
			}

			function ThrowingChild() {
				return <>
					throw new Error('sync boom');
				</>;
			}

			render(App);
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');

			expect(peek(dataA)).toEqual(SUSPENSE_PENDING);
			expect(peek(dataB)).toEqual(SUSPENSE_PENDING);

			// Resolve both promises
			resolve_a!('hello');
			resolve_b!(42);
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// Catch should stay — boundary must NOT switch to resolved
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.a')).toBeNull();
			expect(container.querySelector('.b')).toBeNull();
			expect(settled_a).toEqual({ type: 'resolved', value: 'hello' });
			expect(settled_b).toEqual({ type: 'resolved', value: 42 });

			expect(peek(dataA)).toEqual('hello');
			expect(peek(dataB)).toEqual(42);

			// Verify stability after all microtasks drain
			await new Promise((r) => setTimeout(r, 50));
			flushSync();
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.a')).toBeNull();
			expect(container.querySelector('.b')).toBeNull();
		},
	);

	it(
		'catch stays stable when sync error fires with multiple pending trackAsyncs, one resolves and one rejects',
		async () => {
			let resolve_a: ((v: string) => void) | undefined;
			let reject_b: ((e: any) => void) | undefined;
			let promise_a = new Promise<string>((resolve) => {
				resolve_a = resolve;
			});
			let promise_b = new Promise<number>((_, reject) => {
				reject_b = reject;
			});

			let settled_a: { type: string; value: any } | null = null;
			let settled_b: { type: string; value: any } | null = null;
			promise_a.then(
				(v) => (settled_a = { type: 'resolved', value: v }),
				(e) => (settled_a = { type: 'rejected', value: e }),
			);
			promise_b.then(
				(v) => (settled_b = { type: 'resolved', value: v }),
				(e) => (settled_b = { type: 'rejected', value: e }),
			);

			function App() {
				return <>
					try {
						<ChildA />
						<ChildB />
						<ThrowingChild />
					} pending {
						<p class="pending">{'loading...'}</p>
					} catch (err: Error) {
						<p class="caught">{err.message}</p>
					}
				</>;
			}

			let dataA: any;
			function ChildA() {
				return <>
					dataA = trackAsync(() => promise_a);
					<p class="a">{dataA.value}</p>
				</>;
			}

			let dataB: any;
			function ChildB() {
				return <>
					dataB = trackAsync(() => promise_b);
					<p class="b">{dataB.value}</p>
				</>;
			}

			function ThrowingChild() {
				return <>
					throw new Error('sync boom');
				</>;
			}

			render(App);
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');

			expect(peek(dataA)).toEqual(SUSPENSE_PENDING);
			expect(peek(dataB)).toEqual(SUSPENSE_PENDING);

			// Resolve A, reject B
			resolve_a!('hello');
			reject_b!(new Error('async failure'));
			await new Promise((r) => setTimeout(r, 0));
			flushSync();

			// Catch should stay with original sync error
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.a')).toBeNull();
			expect(container.querySelector('.b')).toBeNull();
			expect(settled_a).toEqual({ type: 'resolved', value: 'hello' });
			expect(settled_b).toEqual({ type: 'rejected', value: expect.any(Error) });

			expect(peek(dataA)).toEqual('hello');
			expect(peek(dataB)).toEqual(SUSPENSE_REJECTED);

			// Verify stability after all microtasks drain
			await new Promise((r) => setTimeout(r, 50));
			flushSync();
			expect(container.querySelector('.caught')?.textContent).toBe('sync boom');
			expect(container.querySelector('.a')).toBeNull();
			expect(container.querySelector('.b')).toBeNull();
		},
	);

	it('try block without catch propagates sync error to upstream boundary', async () => {
		function ThrowingChild() {
			return <>
				throw new Error('no catch here');
				<p>{'should not render'}</p>
			</>;
		}

		function App() {
			return <>
				try {
					<Inner />
				} catch (err) {
					<p class="upstream-caught">{(err as Error).message}</p>
				}
			</>;
		}

		function Inner() {
			return <>
				try {
					<ThrowingChild />
				} pending {
					<p class="inner-pending">{'loading...'}</p>
				}
			</>;
		}

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

		expect(container.querySelector('.upstream-caught')?.textContent).toBe('no catch here');
	});

	it('try block without catch propagates async rejection to upstream boundary', async () => {
		function App() {
			return <>
				try {
					<Inner />
				} catch (err) {
					<p class="upstream-caught">{(err as Error).message}</p>
				}
			</>;
		}

		function Inner() {
			return <>
				try {
					let &[data] = trackAsync(() => Promise.reject(new Error('async no catch')));
					<p class="resolved">{data}</p>
				} pending {
					<p class="inner-pending">{'loading...'}</p>
				}
			</>;
		}

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

		expect(container.querySelector('.upstream-caught')?.textContent).toBe('async no catch');
		expect(container.querySelector('.resolved')).toBeNull();
	});

	it(
		'try block without catch and without pending propagates async rejection to upstream boundary',
		async () => {
			let reject_fn: (error: Error) => void;
			const user_promise = new Promise<string>((_, reject) => {
				reject_fn = reject;
			});

			function App() {
				return <>
					try {
						<Inner />
					} catch (err) {
						<p class="upstream-caught">{(err as Error).message}</p>
					}
				</>;
			}

			function Inner() {
				return <>
					try {
						let &[data] = trackAsync(() => user_promise);
						<p class="resolved">{data}</p>
					} pending {
						<p class="inner-pending">{'loading...'}</p>
					}
				</>;
			}

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

			// Inner should show nothing yet (no pending block)
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.upstream-caught')).toBeNull();

			// Reject the promise
			reject_fn!(new Error('deferred no catch'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.upstream-caught')?.textContent).toBe('deferred no catch');
		},
	);

	it('nested try blocks without catch propagate error through multiple levels', async () => {
		function ThrowingChild() {
			return <>
				throw new Error('deep error');
				<p>{'should not render'}</p>
			</>;
		}

		function App() {
			return <>
				try {
					<Middle />
				} catch (err) {
					<p class="top-caught">{(err as Error).message}</p>
				}
			</>;
		}

		function Middle() {
			return <>
				try {
					<Inner />
				} pending {
					<p class="mid-pending">{'loading...'}</p>
				}
			</>;
		}

		function Inner() {
			return <>
				try {
					<ThrowingChild />
				} pending {
					<p class="inner-pending">{'loading...'}</p>
				}
			</>;
		}

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

		expect(container.querySelector('.top-caught')?.textContent).toBe('deep error');
	});

	it('try block without catch throws to global when no upstream boundary exists', async () => {
		function ThrowingChild() {
			return <>
				throw new Error('unhandled error');
				<p>{'should not render'}</p>
			</>;
		}

		function App() {
			return <>
				try {
					<ThrowingChild />
				} pending {
					<p class="pending">{'loading...'}</p>
				}
			</>;
		}

		expect(() => {
			render(App);
		}).toThrow('unhandled error');
	});

	it(
		'outer try block with pending and inner try with catch and without pending should propagate rejection to inner boundary',
		async () => {
			let reject_fn: (error: Error) => void;
			const user_promise = new Promise<string>((_, reject) => {
				reject_fn = reject;
			});

			function Inner() {
				return <>
					try {
						let &[data] = trackAsync(() => user_promise);
						<p class="resolved">{data}</p>
					} catch (err) {
						<p class="downstream-caught">{(err as Error).message}</p>
					}
				</>;
			}

			function App() {
				return <>
					try {
						<Inner />
					} pending {
						<p class="outer-pending">{'loading...'}</p>
					}
				</>;
			}

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

			// Inner should show nothing yet (no pending block)
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.downstream-caught')).toBeNull();
			expect(container.querySelector('.outer-pending')?.textContent).toBe('loading...');

			// Reject the promise
			reject_fn!(new Error('deferred no catch'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.downstream-caught')?.textContent).toBe('deferred no catch');
			expect(container.querySelector('.outer-pending')).toBeNull();
		},
	);

	it(
		'outer try block with pending and inner try with synchronous catch and without pending should propagate rejection to inner boundary with promise rejecting',
		async () => {
			let reject_fn: (error: Error) => void;
			let resolve_fn: (value: string) => void;
			const user_promise = new Promise<string>((resolve, reject) => {
				resolve_fn = resolve;
				reject_fn = reject;
			});

			let user_settled: { type: string; value: any } | null = null;
			user_promise.then(
				(v) => (user_settled = { type: 'resolved', value: v }),
				(e) => (user_settled = { type: 'rejected', value: e }),
			);

			function Inner() {
				return <>
					try {
						let &[data] = trackAsync(() => user_promise);
						throw new Error('synchronous error');
						<p class="resolved">{data}</p>
					} catch (err) {
						<p class="downstream-caught">{(err as Error).message}</p>
					}
				</>;
			}

			function App() {
				return <>
					try {
						<Inner />
					} pending {
						<p class="outer-pending">{'loading...'}</p>
					}
				</>;
			}

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

			// Inner should show nothing yet (no pending block)
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.outer-pending')).not.toBeNull();
			expect(container.querySelector('.downstream-caught')).toBeNull();

			// Reject the promise
			reject_fn!(new Error('whatever'));
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.outer-pending')).toBeNull();
			expect(container.querySelector('.downstream-caught')?.textContent).toBe('synchronous error');
			expect(user_settled).toEqual({ type: 'rejected', value: expect.any(Error) });
		},
	);

	it(
		'outer try block with pending and inner try with synchronous catch and without pending should propagate rejection to inner boundary with promise resolving instead of rejecting',
		async () => {
			let reject_fn: (error: Error) => void;
			let resolve_fn: (value: string) => void;
			const user_promise = new Promise<string>((resolve, reject) => {
				resolve_fn = resolve;
				reject_fn = reject;
			});

			let user_settled: { type: string; value: any } | null = null;
			user_promise.then(
				(v) => (user_settled = { type: 'resolved', value: v }),
				(e) => (user_settled = { type: 'rejected', value: e }),
			);

			function Inner() {
				return <>
					try {
						let &[data] = trackAsync(() => user_promise);
						throw new Error('synchronous error');
						<p class="resolved">{data}</p>
					} catch (err) {
						<p class="downstream-caught">{(err as Error).message}</p>
					}
				</>;
			}

			function App() {
				return <>
					try {
						<Inner />
					} pending {
						<p class="outer-pending">{'loading...'}</p>
					}
				</>;
			}

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

			// Inner should show nothing yet (no pending block)
			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.outer-pending')).not.toBeNull();
			expect(container.querySelector('.downstream-caught')).toBeNull();

			// Resolve the promise
			resolve_fn!('hello');
			await new Promise((resolve) => setTimeout(resolve, 0));
			flushSync();

			expect(container.querySelector('.resolved')).toBeNull();
			expect(container.querySelector('.outer-pending')).toBeNull();
			// this should render the catch block and make the resolved visible,
			// and the pending of the parent goes away
			expect(container.querySelector('.downstream-caught')?.textContent).toBe('synchronous error');
			expect(user_settled).toEqual({ type: 'resolved', value: 'hello' });
		},
	);
});
