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

describe('server dynamic DOM elements', () => {
	it('renders static dynamic element', async () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
			function App() {
				return <>
					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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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;

		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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 () => {
		function DynamicButton(&{ ...rest }: PropsWithExtras<{
			class: string;
			id: string;
		}>) {
			return <>
				const tag = track('button');
				<@tag {...rest}>{rest.class}</@tag>
				<style>
					.even {
						background-color: green;
					}
					.odd {
						background-color: red;
					}
				</style>
			</>;
		}

		function App() {
			return <>
				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 () => {
		function App() {
			return <>
				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.
			function App() {
				return <>
					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 () => {
		function Child() {
			return <>
				<div class="child">
					<p>{'I am a child component'}</p>
				</div>
				<style>
					.child {
						color: blue;
					}
				</style>
			</>;
		}

		function App() {
			return <>
				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 () => {
		function Child() {
			return <>
				<div class="child">
					<p>{'I am a child component'}</p>
				</div>
				<style>
					.child {
						color: green;
					}
				</style>
			</>;
		}

		function App() {
			return <>
				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);
	});
});
