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

describe('dynamic DOM elements', () => {
	it('renders static dynamic element', () => {
		component 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');
	});

	// The ts errors below are due to limitations in our current tsx generation for dynamic elements.
	// They can be ignored for now. But we'll fix them via jsx() vs <jsx>
	it('renders static dynamic element from a plain object with a tracked property', () => {
		component App() {
			let obj = { tag: track('div') };

			<obj.tag.value>{'Hello World'}</obj.tag.value>
		}
		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', () => {
		component 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',
		() => {
			component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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;

		component 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 ref={...}, {ref ...}, and named ref props on dynamic DOM elements', () => {
		let refAttrElement: HTMLInputElement | null = null;
		let anonymousRefElement: HTMLInputElement | null = null;
		let namedRefElement: HTMLInputElement | null = null;

		component App() {
			let tag = track('input');
			let input: HTMLInputElement | undefined;
			const state: { anonymous?: HTMLInputElement } = {};

			<@tag
				id="dynamic-ref-combo"
				type="text"
				ref={input}
				{ref state.anonymous}
				input_ref={ref (node: HTMLInputElement | null) => {
					namedRefElement = node;
				}}
			/>

			effect(() => {
				refAttrElement = input ?? null;
				anonymousRefElement = state.anonymous ?? null;
			});
		}

		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 dynamic ref forms through a dynamic component that spreads props', () => {
		let refAttrElement: HTMLInputElement | null = null;
		let anonymousRefElement: HTMLInputElement | null = null;
		let namedRefElement: HTMLInputElement | null = null;

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

		component App() {
			let dynamic = track(() => Child);
			let input: HTMLInputElement | undefined;
			const state: { anonymous?: HTMLInputElement } = {};

			<@dynamic
				ref={input}
				{ref state.anonymous}
				input_ref={ref (node: HTMLInputElement | null) => {
					namedRefElement = node;
				}}
			/>

			effect(() => {
				refAttrElement = input ?? null;
				anonymousRefElement = state.anonymous ?? null;
			});
		}

		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('handles dynamic element with createRefKey in spread', () => {
		component 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', () => {
		component 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', () => {
		component 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', () => {
		component 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 ', () => {
		component 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>
		}

		component 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', () => {
		component 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', () => {
		component 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', () => {
		component Child() {
			<div class="child">
				<p>{'I am a child component'}</p>
			</div>

			<style>
				.child {
					color: blue;
				}
			</style>
		}

		component 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', () => {
		component Child() {
			<div class="child">
				<p>{'I am a child component'}</p>
			</div>

			<style>
				.child {
					color: green;
				}
			</style>
		}

		component 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;

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

		component 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;

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

		component 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;

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

		component 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', () => {
		component 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');
	});
});
