/**
 * Race Condition Tests for KTDataTable
 * Tests the fixes for concurrent request handling, request cancellation, and stale response detection
 */

import {
	describe,
	it,
	expect,
	beforeEach,
	afterEach,
	vi,
	type MockedFunction,
} from 'vitest';
import { KTDataTable } from '../datatable';
import { waitFor } from './setup';

describe('KTDataTable Race Condition Fixes', () => {
	let container: HTMLElement;
	let mockFetch: MockedFunction<typeof fetch>;
	let abortSignals: AbortSignal[] = [];

	beforeEach(() => {
		// Setup DOM
		container = document.createElement('div');
		container.innerHTML = `
			<div data-kt-datatable="true">
				<table data-kt-datatable-table="true">
					<thead>
						<tr>
							<th data-kt-datatable-column="id">ID</th>
							<th data-kt-datatable-column="name">Name</th>
						</tr>
					</thead>
					<tbody></tbody>
				</table>
				<div data-kt-datatable-info="true"></div>
				<select data-kt-datatable-size="true"></select>
				<div data-kt-datatable-pagination="true"></div>
			</div>
		`;
		document.body.appendChild(container);

		// Mock fetch to track requests and signals
		abortSignals = [];
		mockFetch = vi.fn<typeof fetch>((_url, options) => {
			// Store abort signal for verification
			if (options?.signal) {
				abortSignals.push(options.signal);
			}

			// Simulate network delay
			return new Promise<Response>((resolve, reject) => {
				const timeout = setTimeout(() => {
					if (options?.signal?.aborted) {
						reject(
							new DOMException('The operation was aborted.', 'AbortError'),
						);
					} else {
						resolve(
							new Response(
								JSON.stringify({
									data: [
										{ id: 1, name: 'Item 1' },
										{ id: 2, name: 'Item 2' },
									],
									totalCount: 2,
								}),
								{
									status: 200,
									headers: { 'Content-Type': 'application/json' },
								},
							),
						);
					}
				}, 100); // 100ms delay

				// Handle abort
				if (options?.signal) {
					options.signal.addEventListener('abort', () => {
						clearTimeout(timeout);
						reject(
							new DOMException('The operation was aborted.', 'AbortError'),
						);
					});
				}
			});
		});

		global.fetch = mockFetch as typeof fetch;
	});

	afterEach(() => {
		document.body.removeChild(container);
		vi.clearAllMocks();
		abortSignals = [];
	});

	describe('AbortController Integration', () => {
		it('should create AbortController for remote data requests', async () => {
			new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
				apiEndpoint: '/api/data',
			});

			await waitFor(150);

			expect(mockFetch).toHaveBeenCalledTimes(1);
			expect(abortSignals.length).toBe(1);
			expect(abortSignals[0]).toBeInstanceOf(AbortSignal);
		});

		it('should use _isFetching flag to prevent concurrent requests', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			// Try to trigger search during initial fetch
			datatable.search('test'); // Should be blocked by _isFetching

			await waitFor(150);

			// Should only have 1 request (initial) because _isFetching blocked the second
			expect(mockFetch).toHaveBeenCalledTimes(1);
		});

		it('should allow new request after previous completes', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			// Wait for initial fetch to complete
			await waitFor(150);

			// Now trigger a search - should succeed
			datatable.search('test');
			await waitFor(150);

			// Should have 2 requests total
			expect(mockFetch).toHaveBeenCalledTimes(2);
			expect(abortSignals.length).toBe(2);
		});

		it('should abort previous request when _performFetchRequest is called again', async () => {
			// This tests the AbortController logic directly by making multiple sequential requests
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(150); // Complete initial request

			// Trigger first search
			datatable.search('first');
			await waitFor(150);

			// Trigger second search (first should complete, second starts fresh)
			datatable.search('second');
			await waitFor(150);

			// Should have 3 requests: initial + first search + second search
			expect(mockFetch).toHaveBeenCalledTimes(3);
			expect(abortSignals.length).toBe(3);

			// Each gets its own AbortController
			abortSignals.forEach((signal) => {
				expect(signal).toBeInstanceOf(AbortSignal);
			});
		});
	});

	describe('Request ID Sequencing', () => {
		it('should assign incremental request IDs for sequential requests', async () => {
			const requestIds: number[] = [];
			let callCount = 0;

			// Mock to capture request sequence
			mockFetch.mockImplementation(
				(_url: RequestInfo | URL, _options?: RequestInit) => {
					callCount++;
					const id = callCount;
					requestIds.push(id);

					return new Promise((resolve) => {
						setTimeout(() => {
							resolve(
								new Response(
									JSON.stringify({
										data: [{ id: id, name: `Item ${id}` }],
										totalCount: 1,
									}),
									{ status: 200 },
								),
							);
						}, 50);
					});
				},
			);

			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(100); // Complete initial request

			datatable.search('a');
			await waitFor(100); // Complete search

			datatable.search('b');
			await waitFor(100); // Complete second search

			// Request IDs should be sequential: 1, 2, 3
			expect(requestIds).toEqual([1, 2, 3]);
		});

		it('should have request ID validation logic in place', async () => {
			// This tests that request IDs are tracked internally
			// The actual stale response scenario is prevented by _isFetching flag
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(150); // Complete initial

			datatable.search('first');
			await waitFor(150); // Complete first search

			datatable.search('second');
			await waitFor(150); // Complete second search

			// All requests should complete successfully with incremental request IDs
			expect(mockFetch).toHaveBeenCalledTimes(3);

			// Table should show data from the last successful request
			const tbody = container.querySelector('tbody');
			expect(tbody?.querySelectorAll('tr').length).toBeGreaterThan(0);
		});
	});

	describe('_isFetching Flag Management', () => {
		it('should prevent concurrent fetch executions', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			// Try to trigger reload immediately (should be blocked by initial fetch)
			datatable.reload(); // Blocked by _isFetching
			datatable.reload(); // Blocked by _isFetching

			await waitFor(150);

			// Should only have 1 request: initial
			// The reload calls are blocked by _isFetching
			expect(mockFetch).toHaveBeenCalledTimes(1);
		});

		it('should reset _isFetching flag after fetch completes', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(150); // Wait for initial fetch

			// Should be able to trigger new fetch after previous completes
			datatable.reload();
			await waitFor(150);

			expect(mockFetch).toHaveBeenCalledTimes(2);
		});

		it('should reset _isFetching flag even after fetch error', async () => {
			let callCount = 0;
			mockFetch.mockImplementation(
				(_url: RequestInfo | URL, _options?: RequestInit) => {
					callCount++;
					if (callCount === 1) {
						// Return invalid JSON to trigger parse error
						return Promise.resolve(new Response('Not JSON', { status: 200 }));
					}
					return Promise.resolve(
						new Response(
							JSON.stringify({
								data: [{ id: 1, name: 'Success' }],
								totalCount: 1,
							}),
							{ status: 200 },
						),
					);
				},
			);

			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(150); // Initial request triggers parse error

			// Should be able to retry after error
			datatable.reload();
			await waitFor(150);

			expect(mockFetch).toHaveBeenCalledTimes(2);
		});
	});

	describe('Loading Spinner Management', () => {
		it('should show spinner during fetch', async () => {
			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			await waitFor(10); // Spinner should be visible

			expect(element.classList.contains('loading')).toBe(true);

			await waitFor(150); // Wait for fetch to complete

			expect(element.classList.contains('loading')).toBe(false);
		});

		it('should keep spinner visible during overlapping requests', async () => {
			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			const datatable = new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			await waitFor(10);
			expect(element.classList.contains('loading')).toBe(true);

			// Trigger second request while first is in progress
			datatable.search('test');
			await waitFor(10);

			// Spinner should still be visible
			expect(element.classList.contains('loading')).toBe(true);

			await waitFor(150);

			// Spinner should hide only after last request completes
			expect(element.classList.contains('loading')).toBe(false);
		});

		it('should not flicker spinner during rapid interactions', async () => {
			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			const datatable = new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			const spinnerStates: boolean[] = [];
			const checkInterval = setInterval(() => {
				spinnerStates.push(element.classList.contains('loading'));
			}, 20);

			await waitFor(10);
			datatable.search('a');
			await waitFor(10);
			datatable.search('ab');
			await waitFor(10);
			datatable.search('abc');

			await waitFor(150);
			clearInterval(checkInterval);

			// Spinner should go from false -> true -> false
			// No flickering (true -> false -> true)
			const transitions = spinnerStates.reduce((acc, curr, idx) => {
				if (idx > 0 && spinnerStates[idx - 1] !== curr) {
					acc.push(curr);
				}
				return acc;
			}, [] as boolean[]);

			// Should have exactly 2 transitions: off->on, on->off
			// No additional transitions that would indicate flickering
			expect(transitions.length).toBeLessThanOrEqual(2);
		});
	});

	describe('Event Handling During Race Conditions', () => {
		it('should fire fetch event for successful requests', async () => {
			const fetchEvents: Event[] = [];

			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			element.addEventListener('fetch', (e) => {
				fetchEvents.push(e);
			});

			const datatable = new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			await waitFor(150); // Complete initial

			datatable.search('test');
			await waitFor(150); // Complete search

			// Should fire fetch for initial and search
			expect(fetchEvents.length).toBeGreaterThanOrEqual(2);
		});

		it('should fire fetched event after successful data load', async () => {
			const fetchedEvents: Event[] = [];

			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			element.addEventListener('fetched', (e) => {
				fetchedEvents.push(e);
			});

			new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			await waitFor(150);

			// Should fire fetched for initial request
			expect(fetchedEvents.length).toBeGreaterThanOrEqual(1);
		});

		it('should not fire error events for AbortError', async () => {
			const errorEvents: Event[] = [];

			const element = container.querySelector(
				'[data-kt-datatable="true"]',
			) as HTMLElement;
			element.addEventListener('error.kt.datatable', (e) => {
				errorEvents.push(e);
			});

			const datatable = new KTDataTable(element, {
				apiEndpoint: '/api/data',
			});

			await waitFor(10);
			datatable.search('test'); // Cancels previous

			await waitFor(150);

			// AbortError should not trigger error event
			expect(errorEvents.length).toBe(0);
		});
	});

	describe('Backward Compatibility', () => {
		it('should work with local data mode (no AbortController needed)', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
			);

			// Should not call fetch for local data
			expect(mockFetch).not.toHaveBeenCalled();

			// Should still work without errors
			datatable.search('Item 1');
			datatable.sort('name');
			datatable.goPage(1);
		});

		it('should maintain existing API compatibility', async () => {
			const datatable = new KTDataTable(
				container.querySelector('[data-kt-datatable="true"]')!,
				{
					apiEndpoint: '/api/data',
				},
			);

			await waitFor(150);

			// All existing methods should work
			expect(() => {
				datatable.reload();
				datatable.search('test');
				datatable.sort('name');
				datatable.goPage(1);
				datatable.setPageSize(20);
				datatable.getState();
			}).not.toThrow();
		});
	});
});
