// src/tests/e2e/auth.e2e.test.ts
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as apiClient from '../../services/apiClient';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import type { UserProfile } from '../../types';

/**
 * @vitest-environment node
 */

describe('Authentication E2E Flow', () => {
  let testUser: UserProfile;
  let testUserAuthToken: string;
  const createdUserIds: string[] = [];

  beforeAll(async () => {
    // Create a user that can be used for login-related tests in this suite.
    try {
      const { user, token } = await createAndLoginUser({
        email: `e2e-login-user-${Date.now()}@example.com`,
        fullName: 'E2E Login User',
      });
      testUserAuthToken = token;
      testUser = user;
      createdUserIds.push(user.user.user_id);
    } catch (error) {
      console.error('[FATAL] Setup failed. DB might be down.', error);
      throw error;
    }
  });

  afterAll(async () => {
    if (createdUserIds.length > 0) {
      await cleanupDb({ userIds: createdUserIds });
    }
  });

  describe('Registration Flow', () => {
    it('should successfully register a new user', async () => {
      const email = `e2e-register-success-${Date.now()}@example.com`;
      const fullName = 'E2E Register User';

      // Act
      const response = await apiClient.registerUser(email, TEST_PASSWORD, fullName);
      const responseBody = await response.json();

      // Assert
      expect(response.status).toBe(201);
      expect(responseBody.data.message).toBe('User registered successfully!');
      expect(responseBody.data.userprofile).toBeDefined();
      expect(responseBody.data.userprofile.user.email).toBe(email);
      expect(responseBody.data.token).toBeTypeOf('string');

      // Add to cleanup
      createdUserIds.push(responseBody.data.userprofile.user.user_id);
    });

    it('should fail to register a user with a weak password', async () => {
      const email = `e2e-register-weakpass-${Date.now()}@example.com`;
      const weakPassword = '123';

      // Act
      const response = await apiClient.registerUser(email, weakPassword, 'Weak Pass User');
      const responseBody = await response.json();

      // Assert
      expect(response.status).toBe(400);
      expect(responseBody.error.details[0].message).toContain(
        'Password must be at least 8 characters long.',
      );
    });

    it('should fail to register a user with a duplicate email', async () => {
      const email = `e2e-register-duplicate-${Date.now()}@example.com`;

      // Act 1: Register the user successfully
      const firstResponse = await apiClient.registerUser(email, TEST_PASSWORD, 'Duplicate User');
      const firstResponseBody = await firstResponse.json();
      expect(firstResponse.status).toBe(201);
      createdUserIds.push(firstResponseBody.data.userprofile.user.user_id);

      // Act 2: Attempt to register the same user again
      const secondResponse = await apiClient.registerUser(email, TEST_PASSWORD, 'Duplicate User');
      const secondResponseBody = await secondResponse.json();

      // Assert
      expect(secondResponse.status).toBe(409); // Conflict
      expect(secondResponseBody.error.message).toContain(
        'A user with this email address already exists.',
      );
    });
  });

  describe('Login Flow', () => {
    it('should successfully log in a registered user', async () => {
      // Act: Attempt to log in with the user created in beforeAll
      const response = await apiClient.loginUser(testUser.user.email, TEST_PASSWORD, false);
      const responseBody = await response.json();

      // Assert
      expect(response.status).toBe(200);
      expect(responseBody.data.userprofile).toBeDefined();
      expect(responseBody.data.userprofile.user.email).toBe(testUser.user.email);
      expect(responseBody.data.token).toBeTypeOf('string');
    });

    it('should fail to log in with an incorrect password', async () => {
      // Act: Attempt to log in with the wrong password
      const response = await apiClient.loginUser(testUser.user.email, 'wrong-password', false);
      const responseBody = await response.json();

      // Assert
      expect(response.status).toBe(401);
      expect(responseBody.error.message).toBe('Incorrect email or password.');
    });

    it('should fail to log in with a non-existent email', async () => {
      const response = await apiClient.loginUser('no-one-here@example.com', TEST_PASSWORD, false);
      const responseBody = await response.json();

      expect(response.status).toBe(401);
      expect(responseBody.error.message).toBe('Incorrect email or password.');
    });

    it('should be able to access a protected route after logging in', async () => {
      // Arrange: Use the token from the beforeAll hook
      const token = testUserAuthToken;
      expect(token).toBeDefined();

      // Act: Use the token to access a protected route
      const profileResponse = await apiClient.getAuthenticatedUserProfile({ tokenOverride: token });
      const responseBody = await profileResponse.json();

      // Assert
      expect(profileResponse.status).toBe(200);
      expect(responseBody.data).toBeDefined();
      expect(responseBody.data.user.user_id).toBe(testUser.user.user_id);
      expect(responseBody.data.user.email).toBe(testUser.user.email);
      expect(responseBody.data.role).toBe('user');
    });

    it('should allow an authenticated user to update their profile', async () => {
      // Arrange: Use the token from the beforeAll hook
      const token = testUserAuthToken;
      expect(token).toBeDefined();

      const profileUpdates = {
        full_name: 'E2E Updated Name',
        avatar_url: 'https://www.projectium.com/updated-avatar.png',
      };

      // Act: Call the update endpoint
      const updateResponse = await apiClient.updateUserProfile(profileUpdates, {
        tokenOverride: token,
      });
      const updateResponseBody = await updateResponse.json();

      // Assert: Check the response from the update call
      expect(updateResponse.status).toBe(200);
      expect(updateResponseBody.data.full_name).toBe(profileUpdates.full_name);
      expect(updateResponseBody.data.avatar_url).toBe(profileUpdates.avatar_url);

      // Act 2: Fetch the profile again to verify persistence
      const verifyResponse = await apiClient.getAuthenticatedUserProfile({ tokenOverride: token });
      const verifyResponseBody = await verifyResponse.json();

      // Assert 2: Check the fetched data
      expect(verifyResponseBody.data.full_name).toBe(profileUpdates.full_name);
      expect(verifyResponseBody.data.avatar_url).toBe(profileUpdates.avatar_url);
    });
  });

  describe('Forgot/Reset Password Flow', () => {
    it('should allow a user to reset their password and log in with the new one', async () => {
      // Arrange: Create a user to reset the password for
      const email = `e2e-reset-pass-${Date.now()}@example.com`;
      const registerResponse = await apiClient.registerUser(
        email,
        TEST_PASSWORD,
        'Reset Pass User',
      );
      const registerResponseBody = await registerResponse.json();
      expect(registerResponse.status).toBe(201);
      createdUserIds.push(registerResponseBody.data.userprofile.user.user_id);

      // Poll until the user can log in, confirming the record has propagated.
      await poll(
        () => apiClient.loginUser(email, TEST_PASSWORD, false),
        (response) => response.ok,
        { timeout: 10000, interval: 1000, description: 'user login after registration' },
      );

      // Request password reset (do not poll, as this endpoint is rate-limited)
      const forgotResponse = await apiClient.requestPasswordReset(email);
      expect(forgotResponse.status).toBe(200);
      const forgotResponseBody = await forgotResponse.json();
      const resetToken = forgotResponseBody.data.token;

      // Assert 1: Check that we received a token.
      expect(
        resetToken,
        'Backend returned 200 but no token. Check backend logs for "Connection terminated" errors.',
      ).toBeDefined();
      expect(resetToken).toBeTypeOf('string');

      // Act 2: Use the token to set a new password.
      const newPassword = 'my-new-e2e-password-!@#$';
      const resetResponse = await apiClient.resetPassword(resetToken, newPassword);
      const resetResponseBody = await resetResponse.json();

      // Assert 2: Check for a successful password reset message.
      expect(resetResponse.status).toBe(200);
      expect(resetResponseBody.data.message).toBe('Password has been reset successfully.');

      // Act 3: Log in with the NEW password
      const loginResponse = await apiClient.loginUser(email, newPassword, false);
      const loginResponseBody = await loginResponse.json();

      expect(loginResponse.status).toBe(200);
      expect(loginResponseBody.data.userprofile).toBeDefined();
      expect(loginResponseBody.data.userprofile.user.email).toBe(email);
    });

    it('should return a generic success message for a non-existent email to prevent enumeration', async () => {
      // Add a small delay to ensure we don't hit the rate limit (5 RPM) if tests run too fast
      await new Promise((resolve) => setTimeout(resolve, 2000));

      const nonExistentEmail = `non-existent-e2e-${Date.now()}@example.com`;
      const response = await apiClient.requestPasswordReset(nonExistentEmail);

      // Check for rate limiting or other errors before parsing JSON to avoid SyntaxError
      if (!response.ok) {
        const text = await response.text();
        throw new Error(`Request failed with status ${response.status}: ${text}`);
      }

      const responseBody = await response.json();
      expect(response.status).toBe(200);
      expect(responseBody.data.message).toBe(
        'If an account with that email exists, a password reset link has been sent.',
      );
      expect(responseBody.data.token).toBeUndefined();
    });
  });

  describe('Token Refresh Flow', () => {
    it('should allow an authenticated user to refresh their access token and use it', async () => {
      // 1. Log in to get the refresh token cookie and an initial access token.
      const loginResponse = await apiClient.loginUser(testUser.user.email, TEST_PASSWORD, false);
      expect(loginResponse.status).toBe(200);
      const loginResponseBody = await loginResponse.json();
      const initialAccessToken = loginResponseBody.data.token;

      // 2. Extract the refresh token from the 'set-cookie' header.
      const setCookieHeader = loginResponse.headers.get('set-cookie');
      expect(
        setCookieHeader,
        'Set-Cookie header should be present in login response',
      ).toBeDefined();
      // A typical Set-Cookie header might be 'refreshToken=...; Path=/; HttpOnly; Max-Age=...'. We just need the 'refreshToken=...' part.
      const refreshTokenCookie = setCookieHeader!.split(';')[0];

      // Wait for >1 second to ensure the 'iat' (Issued At) claim in the new JWT changes.
      // JWT timestamps have second-level precision.
      await new Promise((resolve) => setTimeout(resolve, 1100));

      // 3. Call the refresh token endpoint, passing the cookie.
      // This assumes a new method in apiClient to handle this specific request.
      const refreshResponse = await apiClient.refreshToken(refreshTokenCookie);

      // 4. Assert the refresh was successful and we got a new token.
      expect(refreshResponse.status).toBe(200);
      const refreshResponseBody = await refreshResponse.json();
      const newAccessToken = refreshResponseBody.data.token;
      expect(newAccessToken).toBeDefined();
      expect(newAccessToken).not.toBe(initialAccessToken);

      // 5. Use the new access token to access a protected route.
      const profileResponse = await apiClient.getAuthenticatedUserProfile({
        tokenOverride: newAccessToken,
      });
      expect(profileResponse.status).toBe(200);
      const profileResponseBody = await profileResponse.json();
      expect(profileResponseBody.data.user.user_id).toBe(testUser.user.user_id);
    });

    it('should fail to refresh with an invalid or missing token', async () => {
      // Case 1: No cookie provided. This assumes refreshToken can handle an empty string.
      const noCookieResponse = await apiClient.refreshToken('');
      expect(noCookieResponse.status).toBe(401);

      // Case 2: Invalid cookie provided
      const invalidCookieResponse = await apiClient.refreshToken(
        'refreshToken=invalid-garbage-token',
      );
      expect(invalidCookieResponse.status).toBe(403);
    });
  });
});
