import { track, trackSplit, flushSync, effect, untrack } from 'ripple';

describe('basic client > reactivity', () => {
	it('renders multiple reactive lexical blocks', () => {
		component Basic() {
			<div>
				let obj = {
					count: track(0),
				};

				<span>{obj.@count}</span>
			</div>
			<div>
				let b = {
					count: track(0),
				};

				<button
					onClick={() => {
						b.@count--;
					}}
				>
					{'-'}
				</button>
				<span class="count">{b.@count}</span>
				<button
					onClick={() => {
						b.@count++;
					}}
				>
					{'+'}
				</button>
			</div>
		}
		render(Basic);

		const buttons = container.querySelectorAll('button');

		buttons[0].click();
		flushSync();

		expect(container.querySelector('.count').textContent).toBe('-1');

		buttons[1].click();
		flushSync();

		expect(container.querySelector('.count').textContent).toBe('0');
	});

	it('renders multiple reactive lexical blocks with complexity', () => {
		component Basic() {
			const count = 'count';

			<div>
				let obj = {
					count: track(0),
				};

				<span>{obj[@count]}</span>
			</div>
			<div>
				let b = {
					count: track(0),
				};

				<button
					onClick={() => {
						b[@count]--;
					}}
				>
					{'-'}
				</button>
				<span class="count">{b[@count]}</span>
				<button
					onClick={() => {
						b[@count]++;
					}}
				>
					{'+'}
				</button>
			</div>
		}
		render(Basic);

		const buttons = container.querySelectorAll('button');

		buttons[0].click();
		flushSync();

		expect(container.querySelector('.count').textContent).toBe('-1');

		buttons[1].click();
		flushSync();

		expect(container.querySelector('.count').textContent).toBe('0');
	});

	it('renders with computed reactive state', () => {
		component Basic() {
			let count = track(5);

			<div class="count">{@count}</div>
			<div class="doubled">{@count * 2}</div>
			<div class="is-even">{@count % 2 === 0 ? 'Even' : 'Odd'}</div>
			<button
				onClick={() => {
					@count++;
				}}
			>
				{'Increment'}
			</button>
		}

		render(Basic);

		const countDiv = container.querySelector('.count');
		const doubledDiv = container.querySelector('.doubled');
		const evenDiv = container.querySelector('.is-even');
		const button = container.querySelector('button');

		expect(countDiv.textContent).toBe('5');
		expect(doubledDiv.textContent).toBe('10');
		expect(evenDiv.textContent).toBe('Odd');

		button.click();
		flushSync();

		expect(countDiv.textContent).toBe('6');
		expect(doubledDiv.textContent).toBe('12');
		expect(evenDiv.textContent).toBe('Even');
	});

	it('basic reactivity with standard arrays should work', () => {
		let logs: string[] = [];

		component App() {
			let first = track(0);
			let second = track(0);
			const arr = [first, second];

			const total = track(() => arr.reduce((a, b) => a + @b, 0));

			<button
				onClick={() => {
					@first++;
				}}
			>
				{'first:' + @first}
			</button>
			<button
				onClick={() => {
					@second++;
				}}
			>
				{'second: ' + @second}
			</button>

			effect(() => {
				let _arr: number[] = [];

				arr.forEach((item) => {
					_arr.push(@item);
				});

				logs.push(_arr.join(', '));
			});

			effect(() => {
				if (arr.map((a) => @a).includes(1)) {
					logs.push('arr includes 1');
				}
			});

			<div>{'Sum: ' + @total}</div>
			<div>{'Comma Separated: ' + arr.map((a) => @a).join(', ')}</div>
			<div>{'Number to string: ' + arr.map((a) => String(@a))}</div>
			<div>{'Even numbers: ' + arr.map((a) => @a).filter((a) => a % 2 === 0)}</div>
		}

		render(App);
		flushSync();

		const buttons = container.querySelectorAll('button');
		const divs = container.querySelectorAll('div');

		expect(divs[0].textContent).toBe('Sum: 0');
		expect(divs[1].textContent).toBe('Comma Separated: 0, 0');
		expect(divs[2].textContent).toBe('Number to string: 0,0');
		expect(divs[3].textContent).toBe('Even numbers: 0,0');
		expect(logs).toEqual(['0, 0']);

		buttons[0].click();
		flushSync();

		expect(divs[0].textContent).toBe('Sum: 1');
		expect(divs[1].textContent).toBe('Comma Separated: 1, 0');
		expect(divs[2].textContent).toBe('Number to string: 1,0');
		expect(divs[3].textContent).toBe('Even numbers: 0');
		expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1']);

		buttons[1].click();
		flushSync();

		expect(divs[0].textContent).toBe('Sum: 2');
		expect(divs[1].textContent).toBe('Comma Separated: 1, 1');
		expect(divs[2].textContent).toBe('Number to string: 1,1');
		expect(divs[3].textContent).toBe('Even numbers: ');
		expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1', '1, 1', 'arr includes 1']);
	});

	it('uses track get and set where both mutate value', () => {
		component App() {
			let count = track(0, (v) => v + 1, (v) => v * 2);

			<div class="count">{@count}</div>
			<button
				onClick={() => {
					@count++;
				}}
			>
				{'Increment'}
			</button>
		}

		render(App);

		const countDiv = container.querySelector('.count');
		const button = container.querySelector('button');

		expect(countDiv.textContent).toBe('1');

		button.click();
		flushSync();
		expect(countDiv.textContent).toBe('5');
	});

	it('uses track get and set where set only mutates value', () => {
		component App() {
			let count = track(1, (v) => v, (v) => v * 2);

			<div class="count">{@count}</div>
			<button
				onClick={() => {
					@count++;
				}}
			>
				{'Increment'}
			</button>
		}

		render(App);

		const countDiv = container.querySelector('.count');
		const button = container.querySelector('button');

		expect(countDiv.textContent).toBe('1');

		button.click();
		flushSync();
		expect(countDiv.textContent).toBe('4');
	});

	it('uses track get and set where get only mutates value', () => {
		component App() {
			let count = track(0, (v) => v + 1, (v) => v);

			<div class="count">{@count}</div>
			<button
				onClick={() => {
					@count++;
				}}
			>
				{'Increment'}
			</button>
		}

		render(App);

		const countDiv = container.querySelector('.count');
		const button = container.querySelector('button');

		expect(countDiv.textContent).toBe('1');

		button.click();
		flushSync();
		expect(countDiv.textContent).toBe('3');
	});

	it('passes in next and prev to track set function', () => {
		let logs: number[] = [];

		component App() {
			let count = track(0, (v) => v, (next, prev) => {
				logs.push(prev, next);
				return next;
			});

			<button
				onClick={() => {
					@count++;
				}}
			>
				{'Increment'}
			</button>
		}

		render(App);

		const button = container.querySelector('button');
		button.click();
		flushSync();

		expect(logs).toEqual([0, 1]);
	});

	it('doesn\'t error on mutating a tracked variable in track() setter', () => {
		component Basic() {
			let count = track(0);

			const doubled = track(0, undefined, (value) => {
				@count += value;
				return value;
			});

			<p>{@doubled}</p>
		}

		render(Basic);

		expect(error).toBe(undefined);
	});

	it('unwraps tracked values inside effect', () => {
		let state: { count?: number } = {};

		component Basic() {
			let count = track(0);

			effect(() => {
				state.count = @count;
			});
		}

		render(Basic);
		flushSync();

		expect(state.count).toBe(0);
	});

	it('does not unwrap values with update expressions inside effect', () => {
		let state: {
			initialValue?: number;
			preIncrement?: number;
			postIncrement?: number;
			preDecrement?: number;
			postDecrement?: number;
			finalValue?: number;
		} = {};

		component Basic() {
			let count = track(5);

			effect(() => {
				untrack(() => {
					state.initialValue = @count;
					state.preIncrement = ++@count;
					state.postIncrement = @count++;
					state.preDecrement = --@count;
					state.postDecrement = @count--;
					state.finalValue = @count;
				});
			});
		}

		render(Basic);
		flushSync();

		expect(state.initialValue).toBe(5);
		expect(state.preIncrement).toBe(6);
		expect(state.postIncrement).toBe(6);
		expect(state.preDecrement).toBe(6);
		expect(state.postDecrement).toBe(6);
		expect(state.finalValue).toBe(5);
	});

	describe('track/trackSplit APIs', () => {
		it('errors on invalid value as null for track with trackSplit', () => {
			component App() {
				let message = track('');

				try {
					const [a, b, rest] = trackSplit(null, ['a', 'b']);
				} catch (e) {
					@message = (e as Error).message;
				}

				<pre>{@message}</pre>
			}

			render(App);

			const pre = container.querySelectorAll('pre')[0];
			expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
		});

		it('errors on invalid value as array for track with trackSplit', () => {
			component App() {
				let message = track('');

				try {
					const [a, b, rest] = trackSplit([1, 2, 3], ['a', 'b']);
				} catch (e) {
					@message = (e as Error).message;
				}

				<pre>{@message}</pre>
			}

			render(App);

			const pre = container.querySelectorAll('pre')[0];
			expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
		});

		it('errors on invalid value as tracked for track with trackSplit', () => {
			component App() {
				const t = track({ a: 1, b: 2, c: 3 });
				let message = track('');

				try {
					const [a, b, rest] = trackSplit(t, ['a', 'b']);
				} catch (e) {
					@message = (e as Error).message;
				}

				<pre>{@message}</pre>
			}

			render(App);

			const pre = container.querySelectorAll('pre')[0];
			expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
		});

		it('returns undefined for non-existent props in track with trackSplit', () => {
			component App() {
				const [a, b, rest] = trackSplit({ a: 1, c: 1 }, ['a', 'b']);

				<pre>{@a}</pre>
				<pre>{String(@b)}</pre>
				<pre>{@rest.c}</pre>
			}

			render(App);

			const preA = container.querySelectorAll('pre')[0];
			const preB = container.querySelectorAll('pre')[1];
			const preC = container.querySelectorAll('pre')[2];

			expect(preA.textContent).toBe('1');
			expect(preB.textContent).toBe('undefined');
			expect(preC.textContent).toBe('1');
		});

		it('returns the same tracked object if plain track is called with a tracked object', () => {
			component App() {
				const t = track({ a: 1, b: 2, c: 3 });
				const doublet = track(t);

				<pre>{t === doublet}</pre>
			}

			render(App);

			const pre = container.querySelectorAll('pre')[0];
			expect(pre.textContent).toBe('true');
		});

		it('can retain reactivity for destructure rest via track trackSplit', () => {
			let logs: string[] = [];

			component App() {
				let count = track(0);
				let name = track('Click Me');

				function buttonRef(el: HTMLButtonElement) {
					logs.push('ref called');
					return () => {
						logs.push('cleanup ref');
					};
				}

				<Child
					class="my-button"
					onClick={() => @name === 'Click Me' ? @name = 'Clicked' : @name = 'Click Me'}
					{@count}
					{ref buttonRef}
				>
					{@name}
				</Child>

				<button onClick={() => @count++}>{'Increment Count'}</button>
			}

			component Child(props: PropsWithChildren<{ count: Tracked<number> }>) {
				const [children, count, rest] = trackSplit(props, ['children', 'count']);

				if (@count < 2) {
					<button {...@rest}>
						<@children />
					</button>
				}
				<pre>{@count}</pre>
			}

			render(App);
			flushSync();

			const buttonClickMe = container.querySelectorAll('button')[0];
			const buttonIncrement = container.querySelectorAll('button')[1];
			const countPre = container.querySelector('pre');

			expect(buttonClickMe.textContent).toBe('Click Me');
			expect(countPre.textContent).toBe('0');
			expect(logs).toEqual(['ref called']);

			buttonClickMe.click();
			buttonIncrement.click();
			flushSync();

			expect(buttonClickMe.textContent).toBe('Clicked');
			expect(countPre.textContent).toBe('1');

			buttonIncrement.click();
			flushSync();

			expect(logs).toEqual(['ref called', 'cleanup ref']);
		});
	});
});
