import type { PropsWithExtras } from 'ripple';
import { createRefKey, effect, flushSync, track } from 'ripple';

describe('dynamic DOM elements', () => {
	it('renders static dynamic element', () => {
		function App() @{
			let tag = track('div');
			<{tag}>{'Hello World'}</{tag}>
		}
		render(App);

		const element = container.querySelector('div');
		expect(element).toBeTruthy();
		expect(element.textContent).toBe('Hello World');
	});

	it('renders static dynamic element from a plain object with a tracked property', () => {
		function App() @{
			let obj = { tag: track('div') };
			<{obj.tag}>{'Hello World'}</{obj.tag}>
		}
		render(App);

		const element = container.querySelector('div');
		expect(element).toBeTruthy();
		expect(element.textContent).toBe('Hello World');
	});

	it('renders static dynamic element from a tracked object with a tracked property', () => {
		function App() @{
			let obj = track({ tag: track('div') });
			let tag = obj.value.tag;
			<{tag}>{'Hello World'}</{tag}>
		}
		render(App);

		const element = container.querySelector('div');
		expect(element).toBeTruthy();
		expect(element.textContent).toBe('Hello World');
	});

	it(
		'renders static dynamic element from a tracked object with a computed tracked property',
		() => {
			function App() @{
				let obj = track({ tag: track('div') });
				let tag = obj.value['tag'];
				<{tag}>{'Hello World'}</{tag}>
			}
			render(App);

			const element = container.querySelector('div');
			expect(element).toBeTruthy();
			expect(element.textContent).toBe('Hello World');
		},
	);

	it('renders reactive dynamic element', () => {
		function App() @{
			let &[tag] = track('div');
			<>
				<button
					onClick={() => {
						tag = 'span';
					}}
				>{'Change Tag'}</button>
				<{tag} id="dynamic">{'Hello World'}</{tag}>
			</>
		}
		render(App);

		// Initially should be a div
		let dynamicElement = container.querySelector('#dynamic');
		expect(dynamicElement.tagName).toBe('DIV');
		expect(dynamicElement.textContent).toBe('Hello World');

		// Click button to change tag
		const button = container.querySelector('button');
		button.click();
		flushSync();

		// Should now be a span
		dynamicElement = container.querySelector('#dynamic');
		expect(dynamicElement.tagName).toBe('SPAN');
		expect(dynamicElement.textContent).toBe('Hello World');
	});

	it('renders self-closing dynamic element', () => {
		function App() @{
			let tag = track('input');
			<{tag} type="text" value="test" />
		}
		render(App);

		const element = container.querySelector('input');
		expect(element).toBeTruthy();
		expect(element.type).toBe('text');
		expect(element.value).toBe('test');
	});

	it('handles dynamic element with attributes', () => {
		function App() @{
			let tag = track('div');
			let &[className] = track('test-class');
			<{tag} class={className} id="test" data-testid="dynamic-element">{'Content'}</{tag}>
		}
		render(App);

		const element = container.querySelector('#test');
		expect(element.tagName).toBe('DIV');
		expect(element.className).toBe('test-class');
		expect(element.getAttribute('data-testid')).toBe('dynamic-element');
		expect(element.textContent).toBe('Content');
	});

	it('handles nested dynamic elements', () => {
		function App() @{
			let outerTag = track('div');
			let innerTag = track('span');
			<{outerTag} class="outer">
				<{innerTag} class="inner">{'Nested content'}</{innerTag}>
			</{outerTag}>
		}
		render(App);

		const outer = container.querySelector('.outer');
		const inner = container.querySelector('.inner');

		expect(outer.tagName).toBe('DIV');
		expect(inner.tagName).toBe('SPAN');
		expect(inner.textContent).toBe('Nested content');
		expect(outer.contains(inner)).toBe(true);
	});

	it('handles dynamic element with class object', () => {
		function App() @{
			let tag = track('div');
			let &[active] = track(true);
			<{tag}
				class={{ active: active, 'dynamic-element': true }}
			>{'Element with class object'}</{tag}>
		}
		render(App);

		const element = container.querySelector('div');
		expect(element).toBeTruthy();
		expect(element.classList.contains('active')).toBe(true);
		expect(element.classList.contains('dynamic-element')).toBe(true);
	});

	it('handles dynamic element with style object', () => {
		function App() @{
			let tag = track('span');
			<{tag}
				style={{
					color: 'red',
					fontSize: '16px',
					fontWeight: 'bold',
				}}
			>{'Styled dynamic element'}</{tag}>
		}
		render(App);

		const element = container.querySelector('span');
		expect(element).toBeTruthy();
		expect(element.style.color).toBe('red');
		expect(element.style.fontSize).toBe('16px');
		expect(element.style.fontWeight).toBe('bold');
	});

	it('handles dynamic element with spread attributes', () => {
		function App() @{
			let tag = track('section');
			const attrs = {
				id: 'spread-section',
				'data-testid': 'spread-test',
				class: 'spread-class',
			};
			<{tag} {...attrs} data-extra="additional">{'Element with spread attributes'}</{tag}>
		}
		render(App);

		const element = container.querySelector('section');
		expect(element).toBeTruthy();
		expect(element.id).toBe('spread-section');
		expect(element.getAttribute('data-testid')).toBe('spread-test');
		expect(element.className).toBe('spread-class');
		expect(element.getAttribute('data-extra')).toBe('additional');
	});

	it('handles dynamic element with ref', () => {
		let capturedElement: HTMLElement | null = null;

		function App() @{
			let tag = track('article');
			<{tag}
				ref={(node: HTMLElement) => {
					capturedElement = node;
				}}
				id="ref-test"
			>{'Element with ref'}</{tag}>
		}
		render(App);
		flushSync();

		expect(capturedElement).toBeTruthy();
		expect(capturedElement!.tagName).toBe('ARTICLE');
		expect(capturedElement!.id).toBe('ref-test');
		expect(capturedElement!.textContent).toBe('Element with ref');
	});

	it('handles multiple ref targets on dynamic DOM elements', () => {
		let refAttrElement: HTMLInputElement | null = null;
		let anonymousRefElement: HTMLInputElement | null = null;
		let namedRefElement: HTMLInputElement | null = null;

		function App() @{
			let tag = track('input');
			let input: HTMLInputElement | undefined;
			const state: { anonymous?: HTMLInputElement } = {};
			effect(() => {
				refAttrElement = input ?? null;
				anonymousRefElement = state.anonymous ?? null;
			});
			<{tag}
				id="dynamic-ref-combo"
				type="text"
				ref={[
					input,
					state.anonymous,
					(node: HTMLInputElement | null) => {
						namedRefElement = node;
					},
				]}
			/>
		}

		render(App);
		flushSync();

		const element = container.querySelector('#dynamic-ref-combo');
		expect(element).toBeInstanceOf(HTMLInputElement);
		expect(refAttrElement).toBe(element);
		expect(anonymousRefElement).toBe(element);
		expect(namedRefElement).toBe(element);
		expect(element!.hasAttribute('ref')).toBe(false);
		expect(element!.hasAttribute('input_ref')).toBe(false);
	});

	it('forwards multiple dynamic refs through a dynamic component that spreads props', () => {
		let refAttrElement: HTMLInputElement | null = null;
		let anonymousRefElement: HTMLInputElement | null = null;
		let namedRefElement: HTMLInputElement | null = null;

		function Child(props: PropsWithExtras<{}>) @{
			<input id="dynamic-component-ref-combo" type="text" {...props} />
		}

		function App() @{
			let dynamic = track(() => Child);
			let input: HTMLInputElement | undefined;
			const state: { anonymous?: HTMLInputElement } = {};
			effect(() => {
				refAttrElement = input ?? null;
				anonymousRefElement = state.anonymous ?? null;
			});
			<{dynamic}
				ref={[
					input,
					state.anonymous,
					(node: HTMLInputElement | null) => {
						namedRefElement = node;
					},
				]}
			/>
		}

		render(App);
		flushSync();

		const element = container.querySelector('#dynamic-component-ref-combo');
		expect(element).toBeInstanceOf(HTMLInputElement);
		expect(refAttrElement).toBe(element);
		expect(anonymousRefElement).toBe(element);
		expect(namedRefElement).toBe(element);
		expect(element!.hasAttribute('ref')).toBe(false);
		expect(element!.hasAttribute('input_ref')).toBe(false);
	});

	it('updates forwarded refs when a dynamic component changes', () => {
		let refAttrElement: HTMLInputElement | null = null;
		let anonymousRefElement: HTMLInputElement | null = null;
		let namedRefElement: HTMLInputElement | null = null;

		function TextInput(props: PropsWithExtras<{}>) @{
			<input id="dynamic-component-text" type="text" {...props} />
		}

		function SearchInput(props: PropsWithExtras<{}>) @{
			<input id="dynamic-component-search" type="search" {...props} />
		}

		function App() @{
			let &[dynamic] = track(() => TextInput);
			<>
				<button
					onClick={() => {
						dynamic = dynamic === TextInput ? SearchInput : TextInput;
					}}
				>{'Change Component'}</button>
				<{dynamic}
					ref={[
						(node: HTMLInputElement | null) => {
							refAttrElement = node;
						},
						(node: HTMLInputElement | null) => {
							anonymousRefElement = node;
						},
						(node: HTMLInputElement | null) => {
							namedRefElement = node;
						},
					]}
				/>
			</>
		}

		render(App);
		flushSync();

		const button = container.querySelector('button')!;
		const textInput = container.querySelector('#dynamic-component-text');
		expect(textInput).toBeInstanceOf(HTMLInputElement);
		expect(refAttrElement).toBe(textInput);
		expect(anonymousRefElement).toBe(textInput);
		expect(namedRefElement).toBe(textInput);

		button.click();
		flushSync();

		const searchInput = container.querySelector('#dynamic-component-search');
		expect(searchInput).toBeInstanceOf(HTMLInputElement);
		expect(container.querySelector('#dynamic-component-text')).toBeNull();
		expect(refAttrElement).toBe(searchInput);
		expect(anonymousRefElement).toBe(searchInput);
		expect(namedRefElement).toBe(searchInput);
	});

	it('handles dynamic element with createRefKey in spread', () => {
		function App() @{
			let tag = track('header');
			function elementRef(node: HTMLElement) {
				// Set an attribute on the element to prove ref was called
				node.setAttribute('data-spread-ref-called', 'true');
				node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
			}
			const dynamicProps = {
				id: 'spread-ref-test',
				class: 'ref-element',
				[createRefKey()]: elementRef,
			};
			<{tag} {...dynamicProps}>{'Element with spread ref'}</{tag}>
		}
		render(App);
		flushSync();

		// Check that the spread ref was called by verifying attributes were set
		const element = container.querySelector('header');
		expect(element).toBeTruthy();
		expect(element.getAttribute('data-spread-ref-called')).toBe('true');
		expect(element.getAttribute('data-spread-ref-tag')).toBe('header');
		expect(element.id).toBe('spread-ref-test');
		expect(element.className).toBe('ref-element');
	});

	it('has reactive attributes on dynamic elements', () => {
		function App() @{
			let tag = track('div');
			let &[count] = track(0);
			<>
				<button
					onClick={() => {
						count++;
					}}
				>{'Increment'}</button>
				<{tag}
					id={count % 2 ? 'even' : 'odd'}
					class={count % 2 ? 'even-class' : 'odd-class'}
					data-count={count}
				>
					{'Count: '}
					{count}
				</{tag}>
			</>
		}

		render(App);

		const button = container.querySelector('button');
		const element = container.querySelector('div');

		// Initial state
		expect(element.id).toBe('odd');
		expect(element.className).toBe('odd-class');
		expect(element.getAttribute('data-count')).toBe('0');
		expect(element.textContent).toBe('Count: 0');

		// Click to increment
		button.click();
		flushSync();

		// Attributes should be reactive and update
		expect(element.id).toBe('even');
		expect(element.className).toBe('even-class');
		expect(element.getAttribute('data-count')).toBe('1');
		expect(element.textContent).toBe('Count: 1');

		// Click again
		button.click();
		flushSync();

		// Should toggle back
		expect(element.id).toBe('odd');
		expect(element.className).toBe('odd-class');
		expect(element.getAttribute('data-count')).toBe('2');
		expect(element.textContent).toBe('Count: 2');
	});

	it('applies scoped CSS to dynamic elements', () => {
		function App() @{
			let tag = track('div');
			<>
				<{tag} class="test-class">{'Dynamic element'}</{tag}>
				<style>
					.test-class {
						color: red;
					}
				</style>
			</>
		}

		render(App);

		const element = container.querySelector('div');
		expect(element).toBeTruthy();
		expect(element.classList.contains('test-class')).toBe(true);

		// Check if scoped CSS class is present - THIS MIGHT FAIL if CSS pruning issue exists
		const classes = Array.from(element.classList);
		const hasScopedClass = classes.some((cls) => cls.startsWith('tsrx-'));
		expect(hasScopedClass).toBe(true);
	});

	it('applies scoped CSS to dynamic elements with reactive classes', () => {
		function App() @{
			let tag = track('button');
			let &[count] = track(0);
			<>
				<{tag}
					class={count % 2 ? 'even' : 'odd'}
					id={count % 2 ? 'even' : 'odd'}
					onClick={() => {
						count++;
					}}
				>
					{'Count: '}
					{count}
				</{tag}>
				<style>
					.even {
						background-color: green;
						color: white;
					}
					.odd {
						background-color: red;
						color: white;
					}
				</style>
			</>
		}

		render(App);

		const button = container.querySelector('button');
		expect(button).toBeTruthy();

		// Initial state: should be odd (count=0, 0%2=false)
		expect(button.classList.contains('odd')).toBe(true);
		expect(button.classList.contains('even')).toBe(false);
		expect(button.id).toBe('odd');
		expect(button.textContent).toBe('Count: 0');

		// Check if scoped CSS hash is applied to dynamic element
		const classes = Array.from(button.classList);
		const hasScopedClass = classes.some((cls) => cls.startsWith('tsrx-'));
		expect(hasScopedClass).toBe(true);

		// Click to increment
		button.click();
		flushSync();

		// Should now be even (count=1, 1%2=true)
		expect(button.classList.contains('even')).toBe(true);
		expect(button.classList.contains('odd')).toBe(false);
		expect(button.id).toBe('even');
		expect(button.textContent).toBe('Count: 1');

		// Scoped CSS class should still be present
		const newClasses = Array.from(button.classList);
		const stillHasScopedClass = newClasses.some((cls) => cls.startsWith('tsrx-'));
		expect(stillHasScopedClass).toBe(true);

		// Click again
		button.click();
		flushSync();

		// Should toggle back to odd (count=2, 2%2=false)
		expect(button.classList.contains('odd')).toBe(true);
		expect(button.classList.contains('even')).toBe(false);
		expect(button.id).toBe('odd');
		expect(button.textContent).toBe('Count: 2');
	});

	it('handles spread attributes with class and CSS scoping ', () => {
		function DynamicButton(&{ ...rest }: PropsWithExtras<{
			class: string;
			id: string;
			onClick: EventListener;
		}>) @{
			const tag = track('button');
			<>
				<{tag} {...rest}>{rest.class}</{tag}>
				<style>
					.even {
						background-color: green;
					}
					.odd {
						background-color: red;
					}
				</style>
			</>
		}

		function App() @{
			let &[count] = track(0);
			<DynamicButton
				class={count % 2 ? 'even' : 'odd'}
				id={count % 2 ? 'even' : 'odd'}
				onClick={() => {
					count++;
				}}
			/>
		}

		render(App);

		const button = container.querySelector('button');
		expect(button).toBeTruthy();

		// Initial state: should be odd (count=0, 0%2=false)
		expect(button.classList.contains('odd')).toBe(true);
		expect(button.classList.contains('even')).toBe(false);
		expect(button.id).toBe('odd');

		// Check if scoped CSS hash is applied (this is the critical test)
		const classes = Array.from(button.classList);
		const hasScopedClass = classes.some((cls) => cls.startsWith('tsrx-'));
		expect(hasScopedClass).toBe(true);

		// Click to increment
		button.click();
		flushSync();

		// Should now be even (count=1, 1%2=true)
		expect(button.classList.contains('even')).toBe(true);
		expect(button.classList.contains('odd')).toBe(false);
		expect(button.id).toBe('even');

		// Both classes should still be present
		const newClasses = Array.from(button.classList);
		const stillHasScopedClass = newClasses.some((cls) => cls.startsWith('tsrx-'));
		expect(stillHasScopedClass).toBe(true);
		expect(newClasses.includes('even')).toBe(true);
	});

	it('adds scoping class to dynamic elements', () => {
		function App() @{
			let tag = track('div');
			<>
				<{tag} class="scoped">
					<p>{'Scoped dynamic element'}</p>
				</{tag}>
				<style>
					.scoped {
						color: blue;
					}
				</style>
			</>
		}
		render(App);

		const div = container.querySelector('div');
		const p = div.querySelector('p');

		expect(Array.from(div.classList).some((c) => c.startsWith('tsrx-'))).toBe(true);
		expect(Array.from(p.classList).some((c) => c.startsWith('tsrx-'))).toBe(true);
	});

	it('adds scoping class to dynamic elements when selector targets by tag name', () => {
		function App() @{
			let tag = track('div');
			<>
				<{tag} class="scoped">
					<p>{'Scoped dynamic element'}</p>
				</{tag}>
				<style>
					div {
						color: blue;
					}
				</style>
			</>
		}
		render(App);

		const div = container.querySelector('div');
		const p = div.querySelector('p');

		expect(Array.from(div.classList).some((c) => c.startsWith('tsrx-'))).toBe(true);
		expect(Array.from(p.classList).some((c) => c.startsWith('tsrx-'))).toBe(true);
	});

	it('doesn\'t add scoping class to components inside dynamic element', () => {
		function Child() @{
			<>
				<div class="child">
					<p>{'I am a child component'}</p>
				</div>
				<style>
					.child {
						color: blue;
					}
				</style>
			</>
		}

		function App() @{
			let tag = track('div');
			<>
				<{tag} class="scoped">
					<p>{'Scoped dynamic element'}</p>
					<Child />
				</{tag}>
				<style>
					div {
						color: blue;
					}
				</style>
			</>
		}
		render(App);

		const outerDiv = container.querySelector('.scoped');
		const innerDiv = outerDiv.querySelector('.child');
		const innerP = innerDiv.querySelector('p');

		const outerScope = Array.from(outerDiv.classList).find((c) => c.startsWith('tsrx-'));
		const innerScopes = Array.from(innerDiv.classList).filter((c) => c.startsWith('tsrx-'));
		const innerInnerScopes = Array.from(innerP.classList).filter((c) => c.startsWith('tsrx-'));

		expect(outerScope).toBeTruthy();

		expect(innerScopes).toHaveLength(1);
		expect(innerScopes[0]).not.toBe(outerScope);

		expect(innerInnerScopes).toHaveLength(0);
	});

	it('doesn\'t add scoping class to dynamically rendered component', () => {
		function Child() @{
			<>
				<div class="child">
					<p>{'I am a child component'}</p>
				</div>
				<style>
					.child {
						color: green;
					}
				</style>
			</>
		}

		function App() @{
			let tag = track(() => Child);
			<>
				<{tag} />
				<style>
					.child {
						color: red;
					}
				</style>
			</>
		}
		render(App);

		const div = container.querySelector('.child');
		const p = div.querySelector('p');

		const outerScopes = Array.from(div.classList).filter((c) => c.startsWith('tsrx-'));
		const innerScopes = Array.from(p.classList).filter((c) => c.startsWith('tsrx-'));

		expect(outerScopes).toHaveLength(1);
		expect(innerScopes).toHaveLength(0);
	});

	it('handles ref on dynamic element passed through component with reactive props', () => {
		let capturedElement: HTMLElement | null = null;
		let refCallCount = 0;

		function Button(props: any) @{
			const el = track('button');
			<{el} {...props} />
		}

		function App() @{
			let &[active] = track(false);
			<Button
				data-active={String(active)}
				onClick={() => {
					active = !active;
				}}
				ref={(el: HTMLElement) => {
					capturedElement = el;
					refCallCount++;
				}}
			>{'content'}</Button>
		}

		render(App);
		flushSync();

		expect(capturedElement).toBeTruthy();
		expect(capturedElement!.tagName).toBe('BUTTON');
		expect(capturedElement!.getAttribute('data-active')).toBe('false');
		const initialRefCount = refCallCount;

		// Click the button to trigger reactive prop update
		capturedElement!.click();
		flushSync();

		// After clicking, the reactive prop should update without error
		expect(capturedElement!.getAttribute('data-active')).toBe('true');
		// Ref block should not have been recreated on prop update
		expect(refCallCount).toBe(initialRefCount);
	});

	it('handles ref on dynamic element with spread props containing reactive values', () => {
		let capturedElement: HTMLElement | null = null;

		function Button(props: any) @{
			const el = track('button');
			<{el} {...props} />
		}

		function App() @{
			let &[active] = track(false);
			let &[buttonProps] = track(
				() => ({
					'data-active': active,
				}),
			);
			<Button
				{...buttonProps}
				onClick={() => {
					active = !active;
				}}
				ref={(el: HTMLElement) => {
					capturedElement = el;
				}}
			>
				{'content: '}
				{active}
			</Button>
		}

		render(App);
		flushSync();

		expect(capturedElement).toBeTruthy();
		expect(capturedElement!.tagName).toBe('BUTTON');

		// Click the button to trigger reactive update
		capturedElement!.click();
		flushSync();

		// Should not throw, and ref should still be valid
		expect(capturedElement!.tagName).toBe('BUTTON');
	});

	it('re-establishes ref with cleanup after parent block re-runs', () => {
		let cleanupCount = 0;
		let refCallCount = 0;
		let capturedElement: HTMLElement | null = null;

		function Button(props: any) @{
			const el = track('button');
			<{el} {...props} />
		}

		function App() @{
			let &[active] = track(false);
			<Button
				data-active={String(active)}
				onClick={() => {
					active = !active;
				}}
				ref={(el: HTMLElement) => {
					capturedElement = el;
					refCallCount++;
					return () => {
						cleanupCount++;
					};
				}}
			>{'content'}</Button>
		}

		render(App);
		flushSync();

		expect(capturedElement).toBeTruthy();
		expect(refCallCount).toBe(1);
		expect(cleanupCount).toBe(0);

		// Click to trigger reactive prop update
		capturedElement!.click();
		flushSync();

		// Ref with cleanup should be re-established after parent teardown cycle
		expect(capturedElement!.getAttribute('data-active')).toBe('true');
		expect(refCallCount).toBeGreaterThanOrEqual(1);
	});

	it('should remove and add back a text node in a conditional statement with a tracked', () => {
		function App() @{
			let &[b] = track(true);
			<>
				<div>
					@if (b) {
						<>Inside if</>
					}
				</div>
				<button onClick={() => (b = !b)}>{'Toggle b'}</button>
			</>
		}

		render(App);

		const button = container.querySelector('button');
		const div = container.querySelector('div');

		expect(div.textContent).toBe('Inside if');

		button.click();
		flushSync();

		expect(div.textContent).toBe('');

		button.click();
		flushSync();

		expect(div.textContent).toBe('Inside if');
	});
});
