// src/tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import type { UserProfile } from '../../types';

/**
 * @vitest-environment node
 */

/**
 * These are integration tests that verify the authentication flow against a running backend server.
 * Make sure your Express server is running before executing these tests.
 *
 * To run only these tests: `vitest run src/tests/auth.integration.test.ts`
 */
describe('Authentication API Integration', () => {
  let request: ReturnType<typeof supertest>;
  let testUserEmail: string;
  let testUser: UserProfile;
  const createdUserIds: string[] = [];

  beforeAll(async () => {
    vi.stubEnv('FRONTEND_URL', 'https://example.com');
    const app = (await import('../../../server')).default;
    request = supertest(app);

    // Use a unique email for this test suite to prevent collisions with other tests.
    const email = `auth-integration-test-${Date.now()}@example.com`;
    ({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User', request }));
    testUserEmail = testUser.user.email;
    createdUserIds.push(testUser.user.user_id);
  });

  afterAll(async () => {
    vi.unstubAllEnvs();
    await cleanupDb({ userIds: createdUserIds });
  });

  // This test migrates the logic from the old DevTestRunner.tsx component.
  it('should successfully log in a registered user', async () => {
    // The `rememberMe` parameter is required. For a test, `false` is a safe default.
    const response = await request
      .post('/api/auth/login')
      .send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
    const data = response.body.data;

    if (response.status !== 200) {
      console.error(
        '[DEBUG] Login failed:',
        response.status,
        JSON.stringify(response.body, null, 2),
      );
    }

    // Assert that the API returns the expected structure
    expect(data).toBeDefined();
    expect(response.status).toBe(200);
    expect(data.userprofile).toBeDefined();
    expect(data.userprofile.user.email).toBe(testUserEmail);
    expect(data.userprofile.user.user_id).toBeTypeOf('string');
    expect(data.token).toBeTypeOf('string');
  });

  it('should fail to log in with an incorrect password', async () => {
    // Use the user we just created
    const adminEmail = testUserEmail;
    const wrongPassword = 'wrongpassword';

    // The loginUser function returns a Response object. We check its status.
    const response = await request
      .post('/api/auth/login')
      .send({ email: adminEmail, password: wrongPassword, rememberMe: false });
    expect(response.status).toBe(401);
    const errorData = response.body.error;
    expect(errorData.message).toBe('Incorrect email or password.');
  });

  it('should fail to log in with a non-existent email', async () => {
    const nonExistentEmail = 'nobody-here@example.com';
    const anyPassword = 'any-password';

    // The loginUser function returns a Response object. We check its status.
    const response = await request
      .post('/api/auth/login')
      .send({ email: nonExistentEmail, password: anyPassword, rememberMe: false });
    expect(response.status).toBe(401);
    const errorData = response.body.error;
    // Security best practice: the error message should be identical for wrong password and wrong email
    // to prevent user enumeration attacks.
    expect(errorData.message).toBe('Incorrect email or password.');
  });

  it('should allow registration with an empty string for avatar_url and save it as null', async () => {
    // Arrange: Define user data with an empty avatar_url.
    const email = `empty-avatar-user-${Date.now()}@example.com`;
    const userData = {
      email,
      password: TEST_PASSWORD,
      full_name: 'Empty Avatar',
      avatar_url: '',
    };

    // Act: Register the new user.
    const registerResponse = await request.post('/api/auth/register').send(userData);

    // Assert 1: Check that the registration was successful and the returned profile is correct.
    expect(registerResponse.status).toBe(201);
    const registeredProfile = registerResponse.body.data.userprofile;
    const registeredToken = registerResponse.body.data.token;
    expect(registeredProfile.user.email).toBe(email);
    expect(registeredProfile.avatar_url).toBeNull(); // The API should return null for the avatar_url.

    // Add the newly created user's ID to the array for cleanup in afterAll.
    createdUserIds.push(registeredProfile.user.user_id);

    // Assert 2 (Verification): Fetch the profile using the new token to confirm the value in the DB is null.
    const profileResponse = await request
      .get('/api/users/profile')
      .set('Authorization', `Bearer ${registeredToken}`);

    expect(profileResponse.status).toBe(200);
    expect(profileResponse.body.data.avatar_url).toBeNull();
  });

  it('should successfully refresh an access token using a refresh token cookie', async () => {
    // Arrange: Log in to get a fresh, valid refresh token cookie for this specific test.
    // This ensures the test is self-contained and not affected by other tests.
    const loginResponse = await request
      .post('/api/auth/login')
      .send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
    const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];

    expect(refreshTokenCookie).toBeDefined();

    // Act: Make a request to the refresh-token endpoint, including the cookie.
    const response = await request
      .post('/api/auth/refresh-token')
      .set('Cookie', refreshTokenCookie!);

    // Assert: Check for a successful response and a new access token.
    expect(response.status).toBe(200);
    const data = response.body.data;
    expect(data.token).toBeTypeOf('string');
  });

  it('should fail to refresh an access token with an invalid refresh token cookie', async () => {
    // Arrange: Create a fake/invalid cookie.
    const invalidRefreshTokenCookie = 'refreshToken=this-is-not-a-valid-token';

    // Act: Make a request to the refresh-token endpoint with the invalid cookie.
    const response = await request
      .post('/api/auth/refresh-token')
      .set('Cookie', invalidRefreshTokenCookie);

    // Assert: Check for a 403 Forbidden response.
    expect(response.status).toBe(403);
    const data = response.body.error;
    expect(data.message).toBe('Invalid or expired refresh token.');
  });

  it('should successfully log out and clear the refresh token cookie', async () => {
    // Arrange: Log in to get a valid refresh token cookie.
    const loginResponse = await request
      .post('/api/auth/login')
      .send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
    const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
    expect(refreshTokenCookie).toBeDefined();

    // Act: Make a request to the new logout endpoint, including the cookie.
    const response = await request.post('/api/auth/logout').set('Cookie', refreshTokenCookie!);

    // Assert: Check for a successful response and a cookie-clearing header.
    expect(response.status).toBe(200);
    const logoutSetCookieHeader = response.headers['set-cookie'][0];
    expect(logoutSetCookieHeader).toContain('refreshToken=;');
    expect(logoutSetCookieHeader).toContain('Max-Age=0');
  });

  describe('Rate Limiting', () => {
    it('should block requests to /forgot-password after exceeding the limit', async () => {
      const email = testUserEmail; // Use the user created in beforeAll
      const limit = 5; // Based on the configuration in auth.routes.ts

      // Send requests up to the limit. These should all pass.
      for (let i = 0; i < limit; i++) {
        const response = await request
          .post('/api/auth/forgot-password')
          .set('X-Test-Rate-Limit-Enable', 'true')
          .send({ email });

        // The endpoint returns 200 even for non-existent users to prevent email enumeration.
        expect(response.status).toBe(200);
      }

      // The next request (the 6th one) should be blocked.
      const blockedResponse = await request
        .post('/api/auth/forgot-password')
        .set('X-Test-Rate-Limit-Enable', 'true')
        .send({ email });

      expect(blockedResponse.status).toBe(429);
      expect(blockedResponse.text).toContain(
        'Too many password reset requests from this IP, please try again after 15 minutes.',
      );
    }, 15000); // Increase timeout to handle multiple sequential requests
  });

  describe('Token Edge Cases', () => {
    it('should reject empty Bearer token', async () => {
      const response = await request.get('/api/users/profile').set('Authorization', 'Bearer ');

      expect(response.status).toBe(401);
    });

    it('should reject token without dots (invalid JWT structure)', async () => {
      const response = await request
        .get('/api/users/profile')
        .set('Authorization', 'Bearer notavalidtoken');

      expect(response.status).toBe(401);
    });

    it('should reject token with only 2 parts (missing signature)', async () => {
      const response = await request
        .get('/api/users/profile')
        .set('Authorization', 'Bearer header.payload');

      expect(response.status).toBe(401);
    });

    it('should reject token with invalid signature', async () => {
      // Valid structure but tampered signature
      const response = await request
        .get('/api/users/profile')
        .set('Authorization', 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.invalidsig');

      expect(response.status).toBe(401);
    });

    it('should accept lowercase "bearer" scheme (case-insensitive)', async () => {
      // First get a valid token
      const loginResponse = await request
        .post('/api/auth/login')
        .send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
      const token = loginResponse.body.data.token;

      // Use lowercase "bearer"
      const response = await request
        .get('/api/users/profile')
        .set('Authorization', `bearer ${token}`);

      expect(response.status).toBe(200);
    });

    it('should reject Basic auth scheme', async () => {
      const response = await request
        .get('/api/users/profile')
        .set('Authorization', 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=');

      expect(response.status).toBe(401);
    });

    it('should reject missing Authorization header', async () => {
      const response = await request.get('/api/users/profile');

      expect(response.status).toBe(401);
    });
  });

  describe('Login Security', () => {
    it('should return same error for wrong password and non-existent user', async () => {
      // Wrong password for existing user
      const wrongPassResponse = await request
        .post('/api/auth/login')
        .send({ email: testUserEmail, password: 'wrong-password', rememberMe: false });

      // Non-existent user
      const nonExistentResponse = await request
        .post('/api/auth/login')
        .send({ email: 'nonexistent@example.com', password: 'any-password', rememberMe: false });

      // Both should return 401 with the same message
      expect(wrongPassResponse.status).toBe(401);
      expect(nonExistentResponse.status).toBe(401);
      expect(wrongPassResponse.body.error.message).toBe(nonExistentResponse.body.error.message);
      expect(wrongPassResponse.body.error.message).toBe('Incorrect email or password.');
    });

    it('should return same response for forgot-password on existing and non-existing email', async () => {
      // Request for existing user
      const existingResponse = await request
        .post('/api/auth/forgot-password')
        .send({ email: testUserEmail });

      // Request for non-existing user
      const nonExistingResponse = await request
        .post('/api/auth/forgot-password')
        .send({ email: 'nonexistent-user@example.com' });

      // Both should return 200 with similar success message (prevents email enumeration)
      expect(existingResponse.status).toBe(200);
      expect(nonExistingResponse.status).toBe(200);
      expect(existingResponse.body.success).toBe(true);
      expect(nonExistingResponse.body.success).toBe(true);
    });

    it('should return validation error for missing login fields', async () => {
      const response = await request.post('/api/auth/login').send({ email: testUserEmail }); // Missing password

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
      expect(response.body.error.code).toBe('VALIDATION_ERROR');
    });
  });

  describe('Password Reset', () => {
    it('should reject reset with invalid token', async () => {
      const response = await request.post('/api/auth/reset-password').send({
        token: 'invalid-reset-token',
        newPassword: TEST_PASSWORD,
      });

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
    });
  });

  describe('Registration Validation', () => {
    it('should reject duplicate email registration', async () => {
      const response = await request.post('/api/auth/register').send({
        email: testUserEmail, // Already exists
        password: TEST_PASSWORD,
        full_name: 'Duplicate User',
      });

      expect(response.status).toBe(409); // CONFLICT
      expect(response.body.success).toBe(false);
      expect(response.body.error.code).toBe('CONFLICT');
    });

    it('should reject invalid email format', async () => {
      const response = await request.post('/api/auth/register').send({
        email: 'not-an-email',
        password: TEST_PASSWORD,
        full_name: 'Invalid Email User',
      });

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
      expect(response.body.error.code).toBe('VALIDATION_ERROR');
    });

    it('should reject weak password', async () => {
      const response = await request.post('/api/auth/register').send({
        email: `weak-pass-${Date.now()}@example.com`,
        password: '123456', // Too weak
        full_name: 'Weak Password User',
      });

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
    });
  });

  describe('Refresh Token Edge Cases', () => {
    it('should return error when refresh token cookie is missing', async () => {
      const response = await request.post('/api/auth/refresh-token');

      expect(response.status).toBe(401);
      expect(response.body.error.message).toBe('Refresh token not found.');
    });
  });
});
