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

describe('server dynamic DOM elements', () => {
	it('renders static dynamic element', async () => {
		component App() {
			let tag = track('div');

			<@tag>{'Hello World'}</@tag>
		}

		const { body } = await render(App);

		expect(body).toBeHtml('<div>Hello World</div>');
	});

	// 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', async () => {
		component App() {
			let obj = { tag: track('div') };
			let tag = obj.tag;

			<@tag>{'Hello World'}</@tag>
		}

		const { body } = await render(App);

		expect(body).toBeHtml('<div>Hello World</div>');
	});

	it('renders static dynamic element from a tracked object with a tracked property', async () => {
		component App() {
			let obj = track({ tag: track('div') });
			let tag = obj.value.tag;

			<@tag>{'Hello World'}</@tag>
		}

		const { body } = await render(App);

		expect(body).toBeHtml('<div>Hello World</div>');
	});

	it(
		'renders static dynamic element from a tracked object with a computed tracked property',
		async () => {
			component App() {
				let obj = track({ tag: track('div') });
				let tag = obj.value['tag'];

				<@tag>{'Hello World'}</@tag>
			}

			const { body } = await render(App);

			expect(body).toBeHtml('<div>Hello World</div>');
		},
	);

	it('renders self-closing dynamic element', async () => {
		component App() {
			let tag = track('input');

			<@tag type="text" value="test" />
		}

		const { body } = await render(App);

		expect(body).toBeHtml('<input type="text" value="test" />');
	});

	it('handles dynamic element with attributes', async () => {
		component App() {
			let tag = track('div');
			let &[className] = track('test-class');

			<@tag class={className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
		}
		const { body } = await render(App);
		const { document } = parseHtml(body);

		const div = document.querySelector('div');

		expect(div).toBeTruthy();
		expect(div.className).toBe('test-class');
		expect(div.id).toBe('test');
		expect(div.getAttribute('data-testid')).toBe('dynamic-element');
	});

	it('handles nested dynamic elements', async () => {
		component App() {
			let outerTag = track('div');
			let innerTag = track('span');

			<@outerTag class="outer">
				<@innerTag class="inner">{'Nested content'}</@innerTag>
			</@outerTag>
		}
		const { body } = await render(App);
		const { document } = parseHtml(body);

		const outer = document.querySelector('.outer');
		const inner = document.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', async () => {
		component App() {
			let tag = track('div');
			let &[active] = track(true);

			<@tag class={{ active: active, 'dynamic-element': true }}>{'Element with class object'}</@tag>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const element = document.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', async () => {
		component App() {
			let tag = track('span');

			<@tag
				style={{
					color: 'red',
					fontSize: '16px',
					fontWeight: 'bold',
				}}
			>
				{'Styled dynamic element'}
			</@tag>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const element = document.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', async () => {
		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>
		}
		const { body } = await render(App);
		const { document } = parseHtml(body);

		const element = document.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 not rendered', async () => {
		let capturedElement: HTMLElement | null = null;

		component App() {
			let tag = track('article');

			<@tag
				{ref (node: HTMLElement) => {
					capturedElement = node;
				}}
				id="ref-test"
			>
				{'Element with ref'}
			</@tag>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

		expect(capturedElement).toBeNull();

		capturedElement = document.querySelector('article');

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

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

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const element = document.querySelector('header');

		expect(element).toBeTruthy();
		expect(element.getAttribute('data-spread-ref-called')).toBeNull();
		expect(element.getAttribute('data-spread-ref-tag')).toBeNull();
		expect(element.id).toBe('spread-ref-test');
		expect(element.className).toBe('ref-element');
	});

	it('applies scoped CSS to dynamic elements', async () => {
		component App() {
			let tag = track('div');

			<@tag class="test-class">{'Dynamic element'}</@tag>

			<style>
				.test-class {
					color: red;
				}
			</style>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

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

		const classes = Array.from(element.classList);
		const hasScopedClass = classes.some((cls) => cls.startsWith('tsrx-'));
		expect(hasScopedClass).toBe(true);
	});

	it('handles spread attributes with class and CSS scoping', async () => {
		component DynamicButton(&{ ...rest }: PropsWithExtras<{
			class: string;
			id: string;
		}>) {
			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'} />
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

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

		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);
	});

	it('adds scoping class to dynamic elements', async () => {
		component App() {
			let tag = track('div');

			<@tag class="scoped">
				<p>{'Scoped dynamic element'}</p>
			</@tag>

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

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const div = document.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 and element children if any css is present, even if marked as unused like this tag selector',
		async () => {
			// NOTE: We always add the class scoping hash if there is css
			// but the tag selector will be marked as unused if it's not explicitly seen in the template.
			component App() {
				let tag = track('div');

				<@tag class="scoped">
					<p>{'Scoped dynamic element'}</p>
				</@tag>

				<style>
					div {
						color: blue;
					}
				</style>
			}

			const { body } = await render(App);
			const { document } = parseHtml(body);

			const div = document.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', async () => {
		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>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const outerDiv = document.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', async () => {
		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>
		}

		const { body } = await render(App);
		const { document } = parseHtml(body);

		const div = document.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);
	});
});
