import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

/**
 * Unit tests for the auth service (auth.svelte.ts).
 *
 * The auth module uses Svelte 5 $state runes, so we test it through
 * its public exported API. We mock global fetch and SvelteKit modules
 * to isolate the auth logic.
 */

// Mock $app/navigation before importing the module
vi.mock('$app/navigation', () => ({
	goto: vi.fn()
}));

// Mock $lib/api (getApiBaseUrl)
vi.mock('$lib/api', () => ({
	getApiBaseUrl: () => 'http://test.classeo.local:18000/api'
}));

// Helper: Create a valid-looking JWT token with a given payload
function createTestJwt(payload: Record<string, unknown>): string {
	const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
	const body = btoa(JSON.stringify(payload));
	const signature = 'test-signature';
	return `${header}.${body}.${signature}`;
}

// Helper: Create a JWT with base64url encoding (- and _ instead of + and /)
function createTestJwtUrlSafe(payload: Record<string, unknown>): string {
	const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
		.replace(/\+/g, '-')
		.replace(/\//g, '_')
		.replace(/=+$/, '');
	const body = btoa(JSON.stringify(payload))
		.replace(/\+/g, '-')
		.replace(/\//g, '_')
		.replace(/=+$/, '');
	const signature = 'test-signature';
	return `${header}.${body}.${signature}`;
}

describe('auth service', () => {
	let authModule: typeof import('$lib/auth/auth.svelte');
	const mockGoto = vi.fn();

	beforeEach(async () => {
		vi.clearAllMocks();
		vi.stubGlobal('fetch', vi.fn());

		// Silence expected console.error/warn from error-handling tests
		vi.spyOn(console, 'error').mockImplementation(() => {});
		vi.spyOn(console, 'warn').mockImplementation(() => {});

		// Re-mock goto for each test
		const navModule = await import('$app/navigation');
		(navModule.goto as ReturnType<typeof vi.fn>).mockImplementation(mockGoto);

		// Fresh import to reset $state
		vi.resetModules();
		authModule = await import('$lib/auth/auth.svelte');
	});

	afterEach(() => {
		vi.restoreAllMocks();
	});

	// ==========================================================================
	// isAuthenticated / getAccessToken / getCurrentUserId
	// ==========================================================================
	describe('initial state', () => {
		it('should not be authenticated initially', () => {
			expect(authModule.isAuthenticated()).toBe(false);
		});

		it('should return null access token initially', () => {
			expect(authModule.getAccessToken()).toBeNull();
		});

		it('should return null user ID initially', () => {
			expect(authModule.getCurrentUserId()).toBeNull();
		});
	});

	// ==========================================================================
	// login
	// ==========================================================================
	describe('login', () => {
		it('should return success and set token on successful login', async () => {
			const token = createTestJwt({
				sub: 'user@example.com',
				user_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'password123'
			});

			expect(result.success).toBe(true);
			expect(result.error).toBeUndefined();
			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getAccessToken()).toBe(token);
			expect(authModule.getCurrentUserId()).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
		});

		it('should send credentials with correct format', async () => {
			const token = createTestJwt({ sub: 'test@example.com', user_id: 'test-id' });

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({
				email: 'test@example.com',
				password: 'mypassword',
				captcha_token: 'captcha123'
			});

			expect(fetch).toHaveBeenCalledWith(
				'http://test.classeo.local:18000/api/login',
				expect.objectContaining({
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({
						email: 'test@example.com',
						password: 'mypassword',
						captcha_token: 'captcha123'
					}),
					credentials: 'include'
				})
			);
		});

		it('should return invalid_credentials error on 401', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 401,
				json: () => Promise.resolve({
					type: '/errors/authentication',
					detail: 'Identifiants incorrects',
					attempts: 2,
					delay: 1,
					captchaRequired: false
				})
			});

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'wrong'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('invalid_credentials');
			expect(result.error?.message).toBe('Identifiants incorrects');
			expect(result.error?.attempts).toBe(2);
			expect(result.error?.delay).toBe(1);
			expect(authModule.isAuthenticated()).toBe(false);
		});

		it('should return rate_limited error on 429', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 429,
				json: () => Promise.resolve({
					type: '/errors/rate-limited',
					detail: 'Trop de tentatives',
					retryAfter: 60
				})
			});

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'password'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('rate_limited');
			expect(result.error?.retryAfter).toBe(60);
		});

		it('should return captcha_required error on 428', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 428,
				json: () => Promise.resolve({
					type: '/errors/captcha-required',
					detail: 'CAPTCHA requis',
					attempts: 5,
					captchaRequired: true
				})
			});

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'password'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('captcha_required');
			expect(result.error?.captchaRequired).toBe(true);
		});

		it('should return account_suspended error on 403', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 403,
				json: () => Promise.resolve({
					type: '/errors/account-suspended',
					detail: 'Votre compte a été suspendu'
				})
			});

			const result = await authModule.login({
				email: 'suspended@example.com',
				password: 'password'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('account_suspended');
			expect(result.error?.message).toBe('Votre compte a été suspendu');
		});

		it('should return captcha_invalid error on 400 with captcha-invalid type', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 400,
				json: () => Promise.resolve({
					type: '/errors/captcha-invalid',
					detail: 'CAPTCHA invalide',
					captchaRequired: true
				})
			});

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'password',
				captcha_token: 'invalid-captcha'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('captcha_invalid');
			expect(result.error?.captchaRequired).toBe(true);
		});

		it('should return unknown error when fetch throws', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));

			const result = await authModule.login({
				email: 'user@example.com',
				password: 'password'
			});

			expect(result.success).toBe(false);
			expect(result.error?.type).toBe('unknown');
			expect(result.error?.message).toContain('Erreur de connexion');
		});

		it('should extract user_id from JWT on successful login', async () => {
			const userId = 'b2c3d4e5-f6a7-8901-bcde-f23456789012';
			const token = createTestJwt({
				sub: 'user@test.com',
				user_id: userId
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			expect(authModule.getCurrentUserId()).toBe(userId);
		});

		it('should handle JWT with base64url encoding', async () => {
			const userId = 'c3d4e5f6-a7b8-9012-cdef-345678901234';
			const token = createTestJwtUrlSafe({
				sub: 'urlsafe@test.com',
				user_id: userId
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({ email: 'urlsafe@test.com', password: 'pass' });

			expect(authModule.getCurrentUserId()).toBe(userId);
		});

		it('should set currentUserId to null when token has no user_id claim', async () => {
			const token = createTestJwt({
				sub: 'user@test.com'
				// no user_id claim
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Token is set but user ID extraction should return null
			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getCurrentUserId()).toBeNull();
		});
	});

	// ==========================================================================
	// refreshToken
	// ==========================================================================
	describe('refreshToken', () => {
		it('should set new token on successful refresh', async () => {
			const newToken = createTestJwt({
				sub: 'user@test.com',
				user_id: 'refresh-user-id'
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token: newToken })
			});

			const result = await authModule.refreshToken();

			expect(result).toBe(true);
			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getCurrentUserId()).toBe('refresh-user-id');
		});

		it('should clear token on failed refresh', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 401
			});

			const result = await authModule.refreshToken();

			expect(result).toBe(false);
			expect(authModule.isAuthenticated()).toBe(false);
			expect(authModule.getCurrentUserId()).toBeNull();
		});

		it('should retry on 409 conflict (multi-tab race condition)', async () => {
			const newToken = createTestJwt({
				sub: 'user@test.com',
				user_id: 'retry-user-id'
			});

			// First call returns 409 (token already rotated)
			(fetch as ReturnType<typeof vi.fn>)
				.mockResolvedValueOnce({
					ok: false,
					status: 409
				})
				// Second call succeeds with new cookie
				.mockResolvedValueOnce({
					ok: true,
					json: () => Promise.resolve({ token: newToken })
				});

			const result = await authModule.refreshToken();

			expect(result).toBe(true);
			expect(fetch).toHaveBeenCalledTimes(2);
			expect(authModule.getCurrentUserId()).toBe('retry-user-id');
		});

		it('should fail after max retries on repeated 409', async () => {
			// Three consecutive 409s (max retries is 2)
			(fetch as ReturnType<typeof vi.fn>)
				.mockResolvedValueOnce({ ok: false, status: 409 })
				.mockResolvedValueOnce({ ok: false, status: 409 })
				.mockResolvedValueOnce({ ok: false, status: 409 });

			const result = await authModule.refreshToken();

			expect(result).toBe(false);
			expect(fetch).toHaveBeenCalledTimes(3);
		});

		it('should clear state on network error during refresh', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));

			const result = await authModule.refreshToken();

			expect(result).toBe(false);
			expect(authModule.isAuthenticated()).toBe(false);
			expect(authModule.getCurrentUserId()).toBeNull();
		});

		it('should send refresh request with correct format', async () => {
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: false,
				status: 401
			});

			await authModule.refreshToken();

			expect(fetch).toHaveBeenCalledWith(
				'http://test.classeo.local:18000/api/token/refresh',
				expect.objectContaining({
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					body: '{}',
					credentials: 'include'
				})
			);
		});
	});

	// ==========================================================================
	// logout
	// ==========================================================================
	describe('logout', () => {
		it('should clear token and redirect to login', async () => {
			// First login to set token
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'logout-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });
			expect(authModule.isAuthenticated()).toBe(true);

			// Now logout
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
			await authModule.logout();

			expect(authModule.isAuthenticated()).toBe(false);
			expect(authModule.getAccessToken()).toBeNull();
			expect(authModule.getCurrentUserId()).toBeNull();
			expect(mockGoto).toHaveBeenCalledWith('/login');
		});

		it('should still clear local state even if API call fails', async () => {
			// Login first
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'logout-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Logout with API failure
			(fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));
			await authModule.logout();

			expect(authModule.isAuthenticated()).toBe(false);
			expect(authModule.getAccessToken()).toBeNull();
			expect(mockGoto).toHaveBeenCalledWith('/login');
		});

		it('should call onLogout callback when registered', async () => {
			const logoutCallback = vi.fn();
			authModule.onLogout(logoutCallback);

			// Login first
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'callback-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Logout
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
			await authModule.logout();

			expect(logoutCallback).toHaveBeenCalledOnce();
		});
	});

	// ==========================================================================
	// authenticatedFetch
	// ==========================================================================
	describe('authenticatedFetch', () => {
		it('should add Authorization header with Bearer token', async () => {
			// Login to set token
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'auth-fetch-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Make authenticated request
			const mockResponse = { ok: true, status: 200 };
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockResponse);

			await authModule.authenticatedFetch('http://test.classeo.local:18000/api/users');

			// Second call should be the authenticated request (first was login)
			const calls = (fetch as ReturnType<typeof vi.fn>).mock.calls;
			expect(calls.length).toBeGreaterThanOrEqual(2);
			const lastCall = calls[1]!;
			expect(lastCall[0]).toBe('http://test.classeo.local:18000/api/users');

			const headers = lastCall[1]?.headers as Headers;
			expect(headers).toBeDefined();
			// Headers is a Headers object
			expect(headers.get('Authorization')).toBe(`Bearer ${token}`);
		});

		it('should attempt refresh when no token is available', async () => {
			// No login - token is null
			// First fetch call will be the refresh attempt
			const refreshToken = createTestJwt({ sub: 'user@test.com', user_id: 'refreshed-user' });
			(fetch as ReturnType<typeof vi.fn>)
				// Refresh call succeeds
				.mockResolvedValueOnce({
					ok: true,
					json: () => Promise.resolve({ token: refreshToken })
				})
				// Then the actual request succeeds
				.mockResolvedValueOnce({ ok: true, status: 200 });

			await authModule.authenticatedFetch('http://test.classeo.local:18000/api/data');

			// Should have made 2 calls: refresh + actual request
			expect(fetch).toHaveBeenCalledTimes(2);
		});

		it('should retry with refresh on 401 response', async () => {
			// Login first
			const oldToken = createTestJwt({ sub: 'user@test.com', user_id: 'old-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token: oldToken })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Request returns 401
			const newToken = createTestJwt({ sub: 'user@test.com', user_id: 'new-user' });
			(fetch as ReturnType<typeof vi.fn>)
				// First request returns 401
				.mockResolvedValueOnce({ ok: false, status: 401 })
				// Refresh succeeds
				.mockResolvedValueOnce({
					ok: true,
					json: () => Promise.resolve({ token: newToken })
				})
				// Retried request succeeds
				.mockResolvedValueOnce({ ok: true, status: 200 });

			const response = await authModule.authenticatedFetch('http://test.classeo.local:18000/api/data');

			expect(response.ok).toBe(true);
		});

		it('should redirect to login if refresh fails during 401 retry', async () => {
			// Login first
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'expired-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Request returns 401 and refresh also fails
			(fetch as ReturnType<typeof vi.fn>)
				.mockResolvedValueOnce({ ok: false, status: 401 })
				.mockResolvedValueOnce({ ok: false, status: 401 });

			await expect(
				authModule.authenticatedFetch('http://test.classeo.local:18000/api/data')
			).rejects.toThrow('Session expired');

			expect(mockGoto).toHaveBeenCalledWith('/login');
		});
	});

	// ==========================================================================
	// JWT edge cases (tested through login)
	// ==========================================================================
	describe('JWT parsing edge cases', () => {
		it('should handle token with non-string user_id', async () => {
			// user_id is a number instead of string
			const token = createTestJwt({
				sub: 'user@test.com',
				user_id: 12345
			});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Should return null because user_id is not a string
			expect(authModule.getCurrentUserId()).toBeNull();
			// But token should still be set
			expect(authModule.isAuthenticated()).toBe(true);
		});

		it('should handle token with empty payload', async () => {
			const token = createTestJwt({});

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			expect(authModule.getCurrentUserId()).toBeNull();
			expect(authModule.isAuthenticated()).toBe(true);
		});

		it('should handle malformed token (not 3 parts)', async () => {
			const malformedToken = 'not.a.valid.jwt.token';

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token: malformedToken })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Token is stored but user ID extraction fails
			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getCurrentUserId()).toBeNull();
		});

		it('should handle token with invalid base64 payload', async () => {
			const invalidToken = 'header.!!!invalid-base64!!!.signature';

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token: invalidToken })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getCurrentUserId()).toBeNull();
		});

		it('should handle token with valid base64 but invalid JSON', async () => {
			const header = btoa(JSON.stringify({ alg: 'HS256' }));
			const body = btoa('not-json-content');
			const invalidJsonToken = `${header}.${body}.signature`;

			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token: invalidJsonToken })
			});

			await authModule.login({ email: 'user@test.com', password: 'pass' });

			expect(authModule.isAuthenticated()).toBe(true);
			expect(authModule.getCurrentUserId()).toBeNull();
		});
	});

	// ==========================================================================
	// onLogout callback
	// ==========================================================================
	describe('onLogout', () => {
		it('should allow registering a logout callback', () => {
			const callback = vi.fn();
			// Should not throw
			authModule.onLogout(callback);
		});

		it('should invoke callback before clearing state during logout', async () => {
			let wasAuthenticatedDuringCallback = false;
			const callback = vi.fn(() => {
				// Check auth state at the moment the callback fires
				wasAuthenticatedDuringCallback = authModule.isAuthenticated();
			});

			authModule.onLogout(callback);

			// Login
			const token = createTestJwt({ sub: 'user@test.com', user_id: 'cb-user' });
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ token })
			});
			await authModule.login({ email: 'user@test.com', password: 'pass' });

			// Logout
			(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
			await authModule.logout();

			expect(callback).toHaveBeenCalledOnce();
			// The callback fires before accessToken is set to null
			expect(wasAuthenticatedDuringCallback).toBe(true);
		});
	});
});
