/**
 * Mock implementation of the Fetch API for testing
 */

class MockResponse {
    constructor(body, init = {}) {
        this.body = body;
        this.status = init.status || 200;
        this.ok = this.status >= 200 && this.status < 300;
        this.statusText = init.statusText || '';
        this.headers = new Map(Object.entries(init.headers || {}));
        this.url = init.url || '';
        this.type = init.type || 'basic';
    }

    async json() {
        if (typeof this.body === 'string') {
            return JSON.parse(this.body);
        }
        return this.body;
    }

    async text() {
        if (typeof this.body === 'string') {
            return this.body;
        }
        return JSON.stringify(this.body);
    }

    async blob() {
        return new Blob([await this.text()]);
    }

    async arrayBuffer() {
        const text = await this.text();
        const encoder = new TextEncoder();
        return encoder.encode(text).buffer;
    }

    clone() {
        return new MockResponse(this.body, {
            status: this.status,
            statusText: this.statusText,
            headers: Object.fromEntries(this.headers),
            url: this.url,
            type: this.type
        });
    }
}

class FetchMock {
    constructor() {
        this.reset();
    }

    reset() {
        // Abort all pending requests before clearing
        if (this.pendingRequests) {
            this.pendingRequests.forEach(({ controller }) => {
                if (controller && !controller.signal.aborted) {
                    controller.abort();
                }
            });
        }
        this.calls = [];
        this.responses = [];
        this.pendingRequests = [];
    }

    // Record a fetch call
    recordCall(url, options) {
        this.calls.push({ url, request: options });
    }

    // Get all recorded calls
    getCalls() {
        return this.calls;
    }

    // Get the last call
    getLastCall() {
        return this.calls[this.calls.length - 1];
    }

    // Mock a response for a specific URL pattern
    mockResponse(method, urlPattern, response, options = {}) {
        let upperCasedMethod = method.toUpperCase();
        if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(upperCasedMethod) < 0) {
            throw Error("Invalid HTTP method: " + method)
        }
        if (typeof response === 'string') {
            let str = response;
            response = new MockResponse(str, options);
        }
        this.responses.push({
            method: upperCasedMethod,
            urlPattern: typeof urlPattern === 'string' ? new RegExp(urlPattern) : urlPattern,
            response,
            request: options,
            once: options.once || false,
            used: false
        });
    }

    // Mock a successful JSON response
    mockJsonResponse(method, urlPattern, data, status = 200) {
        this.mockResponse(method, urlPattern, new MockResponse(data, {
            status,
            headers: { 'Content-Type': 'application/json' }
        }));
    }

    // Mock an error response
    mockErrorResponse(method, urlPattern, status = 500, message = 'Server Error') {
        this.mockResponse(method, urlPattern, new MockResponse({ error: message }, { status }));
    }

    // Mock a network error
    mockNetworkError(method, urlPattern, error = new Error('Network Error')) {
        this.mockResponse(method, urlPattern, Promise.reject(error));
    }

    // Mock a network failure (simpler alias)
    mockFailure(method, urlPattern, message = 'Network failure') {
        this.mockNetworkError(method, urlPattern, new Error(message));
    }

    // Mock sequential responses that don't resolve until explicitly released
    // Returns an object with next() to release requests one at a time
    mockSequentialResponses(method, urlPattern, response, options = {}) {
        const pendingQueue = [];  // Requests waiting to be resolved
        const waiters = [];       // next() calls waiting for requests

        // Register a response function that holds requests
        this.mockResponse(method, urlPattern, () => {
            return new Promise(resolve => {
                const responseBody = typeof response === 'string'
                    ? new MockResponse(response, options)
                    : response;

                // If there's already a waiter, resolve immediately
                if (waiters.length > 0) {
                    const waiter = waiters.shift();
                    resolve(responseBody);
                    // Wait for htmx to finish processing, then resolve waiter
                    setTimeout(() => waiter(), 0);
                } else {
                    // Queue this request to be released later
                    pendingQueue.push({ resolve, responseBody });
                }
            });
        });

        return {
            // Release next pending request and wait for htmx to finish processing
            next() {
                return new Promise(resolve => {
                    if (pendingQueue.length > 0) {
                        // Request already waiting, release it
                        const item = pendingQueue.shift();
                        item.resolve(item.responseBody);
                        // Give htmx time to process the response
                        setTimeout(resolve, 0);
                    } else {
                        // No request yet, wait for one to arrive
                        waiters.push(resolve);
                    }
                });
            },
            // Get count of pending requests
            get pendingCount() {
                return pendingQueue.length;
            }
        };
    }

    // Find matching response
    findResponse(method, url) {
        for (let i = this.responses.length - 1; i >= 0; i--) {
            const mock = this.responses[i];
            if (mock.method === method && mock.urlPattern.test(url)) {
                if (mock.once) {
                    if (!mock.used) {
                        mock.used = true;
                        return typeof mock.response === 'function' ? mock.response() : mock.response;
                    }
                } else {
                    return typeof mock.response === 'function' ? mock.response() : mock.response;
                }
            }
        }
        console.error("no response configured for ", url, " available responses: ", this.responses);
        return "NO RESPONSE CONFIGURED FOR " + url;
    }

    // Wait for all pending requests to complete
    async waitForRequests() {
        if (this.pendingRequests.length === 0) {
            return Promise.resolve();
        }
        await Promise.all(this.pendingRequests.map(req => req.promise));
    }

    // The actual fetch mock function
    fetch(url, options = {}) {
        this.recordCall(url, options);
        options.method = options.method.toUpperCase()
        const response = this.findResponse(options.method, url);

        // Create an AbortController for this request
        const controller = new AbortController();

        // Create a tracking object for this request
        const pendingRequest = { controller, promise: null };

        // Create a promise to track this request
        const requestPromise = new Promise((resolve, reject) => {
            // Check if already aborted
            if (controller.signal.aborted) {
                reject(new DOMException('The operation was aborted', 'AbortError'));
                return;
            }

            // Listen for abort
            controller.signal.addEventListener('abort', () => {
                reject(new DOMException('The operation was aborted', 'AbortError'));
            });

            // Process the response
            Promise.resolve(response)
                .then(result => {
                    if (typeof result === 'string') result = new MockResponse(result);
                    if (!controller.signal.aborted) {
                        resolve(result);
                    }
                })
                .catch(error => {
                    if (!controller.signal.aborted) {
                        reject(error);
                    }
                });
        })
        .finally(() => {
            // Remove from pending requests when done
            const index = this.pendingRequests.indexOf(pendingRequest);
            if (index > -1) {
                this.pendingRequests.splice(index, 1);
            }
        });

        // Store the promise in the tracking object
        pendingRequest.promise = requestPromise;

        // Track this pending request
        this.pendingRequests.push(pendingRequest);

        return requestPromise;
    }
}

// Create a global instance
const fetchMock = new FetchMock();

// Install the mock globally
function installFetchMock() {
    globalThis.fetch = fetchMock.fetch.bind(fetchMock);
}

// Uninstall the mock and restore original fetch
let originalFetch;
function uninstallFetchMock() {
    if (originalFetch) {
        globalThis.fetch = originalFetch;
    }
}

// Save original fetch before installing
if (typeof globalThis.fetch !== 'undefined') {
    originalFetch = globalThis.fetch;
}