// src/tests/integration/user.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest';
import path from 'path';
import fs from 'node:fs/promises';
import type { UserProfile, MasterGroceryItem, ShoppingList } from '../../types';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import { cleanupFiles } from '../utils/cleanupFiles';

/**
 * @vitest-environment node
 */

describe('User API Routes Integration Tests', () => {
  let request: ReturnType<typeof supertest>;
  let testUser: UserProfile;
  let authToken: string;
  const createdUserIds: string[] = [];
  const createdMasterItemIds: number[] = [];

  // Before any tests run, create a new user and log them in.
  // The token will be used for all subsequent API calls in this test suite.
  beforeAll(async () => {
    vi.stubEnv('FRONTEND_URL', 'https://example.com');
    const app = (await import('../../../server')).default;
    request = supertest(app);

    const email = `user-test-${Date.now()}@example.com`;
    const { user, token } = await createAndLoginUser({ email, fullName: 'Test User', request });
    testUser = user;
    authToken = token;
    createdUserIds.push(user.user.user_id);
  });

  // After all tests, clean up by deleting the created user.
  // This now cleans up ALL users created by this test suite to prevent pollution.
  afterAll(async () => {
    vi.unstubAllEnvs();
    await cleanupDb({
      userIds: createdUserIds,
      masterItemIds: createdMasterItemIds,
    });

    // Safeguard to clean up any avatar files created during tests.
    const uploadDir = path.resolve(__dirname, '../../../uploads/avatars');
    try {
      const allFiles = await fs.readdir(uploadDir);
      // Filter for any file that contains any of the user IDs created in this test suite.
      const testFiles = allFiles
        .filter((f) => createdUserIds.some((userId) => userId && f.includes(userId)))
        .map((f) => path.join(uploadDir, f));

      if (testFiles.length > 0) {
        await cleanupFiles(testFiles);
      }
    } catch (error) {
      // Ignore if the directory doesn't exist, but log other errors.
      if (error instanceof Error && (error as NodeJS.ErrnoException).code !== 'ENOENT') {
        console.error('Error during user integration test avatar file cleanup:', error);
      }
    }
  });

  it('should fetch the authenticated user profile via GET /api/users/profile', async () => {
    // Act: Call the API endpoint using the authenticated token.
    const response = await request
      .get('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`);
    const profile = response.body.data;

    // Assert: Verify the profile data matches the created user.
    expect(response.status).toBe(200);
    expect(profile.user.user_id).toBe(testUser.user.user_id);
    expect(profile.user.email).toBe(testUser.user.email); // This was already correct
    expect(profile.full_name).toBe('Test User');
    expect(profile.role).toBe('user');
  });

  it('should update the user profile via PUT /api/users/profile', async () => {
    // Arrange: Define the profile updates.
    const profileUpdates = {
      full_name: 'Updated Test User',
    };

    // Act: Call the update endpoint with the new data and the auth token.
    const response = await request
      .put('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .send(profileUpdates);
    const updatedProfile = response.body.data;

    // Assert: Check that the returned profile reflects the changes.
    expect(response.status).toBe(200);
    expect(updatedProfile.full_name).toBe('Updated Test User');

    // Also, fetch the profile again to ensure the change was persisted.
    const refetchResponse = await request
      .get('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`);
    const refetchedProfile = refetchResponse.body.data;
    expect(refetchedProfile.full_name).toBe('Updated Test User');
  });

  it('should allow updating the profile with an empty string for avatar_url', async () => {
    // Arrange: Define the profile updates.
    const profileUpdates = {
      full_name: 'Empty Avatar User',
      avatar_url: '',
    };

    // Act: Call the update endpoint with the new data and the auth token.
    const response = await request
      .put('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .send(profileUpdates);
    const updatedProfile = response.body.data;

    // Assert: Check that the returned profile reflects the changes.
    expect(response.status).toBe(200);
    expect(updatedProfile.full_name).toBe('Empty Avatar User');
    expect(updatedProfile.avatar_url).toBeNull();

    // Also, fetch the profile again to ensure the change was persisted in the database as NULL.
    const refetchResponse = await request
      .get('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`);
    expect(refetchResponse.body.data.avatar_url).toBeNull();
  });

  it('should update user preferences via PUT /api/users/profile/preferences', async () => {
    // Arrange: Define the preference updates.
    const preferenceUpdates = {
      darkMode: true,
    };

    // Act: Call the update endpoint.
    const response = await request
      .put('/api/users/profile/preferences')
      .set('Authorization', `Bearer ${authToken}`)
      .send(preferenceUpdates);
    const updatedProfile = response.body.data;

    // Assert: Check that the preferences object in the returned profile is updated.
    expect(response.status).toBe(200);
    expect(updatedProfile.preferences?.darkMode).toBe(true);
  });

  it('should reject registration with a weak password', async () => {
    // Arrange: Define a new user with a known weak password.
    const email = `weak-password-user-${Date.now()}@example.com`;
    const weakPassword = 'password';

    // Act & Assert: Attempt to register and expect the promise to reject
    // with an error message indicating the password is too weak.
    const response = await request.post('/api/auth/register').send({
      email,
      password: weakPassword,
      full_name: 'Weak Password User',
    });

    expect(response.status).toBe(400);
    const errorData = response.body.error as { message: string; details: { message: string }[] };
    // For validation errors, the detailed messages are in the `details` array.
    // We join them to check for the specific feedback from the password strength checker.
    const detailedErrorMessage = errorData.details?.map((e) => e.message).join(' ');
    expect(detailedErrorMessage).toMatch(/Password is too weak/);
  });

  it('should allow a user to delete their own account and then fail to log in', async () => {
    // Arrange: Create a new, separate user just for this deletion test.
    const deletionEmail = `delete-me-${Date.now()}@example.com`;
    const { user: deletionUser, token: deletionToken } = await createAndLoginUser({
      email: deletionEmail,
      request,
    });
    createdUserIds.push(deletionUser.user.user_id);

    // Act: Call the delete endpoint with the correct password and token.
    const response = await request
      .delete('/api/users/account')
      .set('Authorization', `Bearer ${deletionToken}`)
      .send({ password: TEST_PASSWORD });
    const deleteResponse = response.body;

    // Assert: Check for a successful deletion message.
    expect(response.status).toBe(200);
    expect(deleteResponse.data.message).toBe('Account deleted successfully.');

    // Assert (Verification): Attempting to log in again with the same credentials should now fail.
    const loginResponse = await request
      .post('/api/auth/login')
      .send({ email: deletionEmail, password: TEST_PASSWORD });
    expect(loginResponse.status).toBe(401);
    const errorData = loginResponse.body.error;
    expect(errorData.message).toBe('Incorrect email or password.');
  });

  it('should allow a user to reset their password and log in with the new one', async () => {
    // Arrange: Create a new user for the password reset flow.
    const resetEmail = `reset-me-${Date.now()}@example.com`;
    const { user: resetUser } = await createAndLoginUser({ email: resetEmail, request });
    createdUserIds.push(resetUser.user.user_id);

    // Act 1: Request a password reset. In our test environment, the token is returned in the response.
    const resetRequestRawResponse = await request
      .post('/api/auth/forgot-password')
      .send({ email: resetEmail });
    if (resetRequestRawResponse.status !== 200) {
      const errorData = resetRequestRawResponse.body;
      throw new Error(errorData.message || 'Password reset request failed');
    }
    const resetRequestResponse = resetRequestRawResponse.body.data;
    const resetToken = resetRequestResponse.token;

    // Assert 1: Check that we received a token.
    expect(resetToken).toBeDefined();
    expect(resetToken).toBeTypeOf('string');

    // Act 2: Use the token to set a new password.
    const newPassword = 'my-new-secure-password-!@#$';
    const resetRawResponse = await request
      .post('/api/auth/reset-password')
      .send({ token: resetToken!, newPassword });
    if (resetRawResponse.status !== 200) {
      const errorData = resetRawResponse.body;
      throw new Error(errorData.message || 'Password reset failed');
    }
    const resetResponse = resetRawResponse.body.data;

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

    // Act 3 & Assert 3 (Verification): Log in with the NEW password to confirm the change.
    const loginResponse = await request
      .post('/api/auth/login')
      .send({ email: resetEmail, password: newPassword });
    const loginData = loginResponse.body.data;
    expect(loginData.userprofile).toBeDefined();
    expect(loginData.userprofile.user.user_id).toBe(resetUser.user.user_id);
  });

  describe('User Data Routes (Watched Items & Shopping Lists)', () => {
    it('should allow a user to add and remove a watched item', async () => {
      // Act 1: Add a new watched item. The API returns the created master item.
      const addResponse = await request
        .post('/api/users/watched-items')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ itemName: 'Integration Test Item', category: 'Other/Miscellaneous' });
      const newItem = addResponse.body.data;

      if (newItem?.master_grocery_item_id)
        createdMasterItemIds.push(newItem.master_grocery_item_id);
      // Assert 1: Check that the item was created correctly.
      expect(addResponse.status).toBe(201);
      expect(newItem.name).toBe('Integration Test Item');

      // Act 2: Fetch all watched items for the user.
      const watchedItemsResponse = await request
        .get('/api/users/watched-items')
        .set('Authorization', `Bearer ${authToken}`);
      const watchedItems = watchedItemsResponse.body.data;

      // Assert 2: Verify the new item is in the user's watched list.
      expect(
        watchedItems.some(
          (item: MasterGroceryItem) =>
            item.master_grocery_item_id === newItem.master_grocery_item_id,
        ),
      ).toBe(true);

      // Act 3: Remove the watched item.
      const removeResponse = await request
        .delete(`/api/users/watched-items/${newItem.master_grocery_item_id}`)
        .set('Authorization', `Bearer ${authToken}`);
      expect(removeResponse.status).toBe(204);

      // Assert 3: Fetch again and verify the item is gone.
      const finalWatchedItemsResponse = await request
        .get('/api/users/watched-items')
        .set('Authorization', `Bearer ${authToken}`);
      const finalWatchedItems = finalWatchedItemsResponse.body.data;
      expect(
        finalWatchedItems.some(
          (item: MasterGroceryItem) =>
            item.master_grocery_item_id === newItem.master_grocery_item_id,
        ),
      ).toBe(false);
    });

    it('should allow a user to manage a shopping list', async () => {
      // Act 1: Create a new shopping list.
      const createListResponse = await request
        .post('/api/users/shopping-lists')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'My Integration Test List' });
      const newList = createListResponse.body.data;

      // Assert 1: Check that the list was created.
      expect(createListResponse.status).toBe(201);
      expect(newList.name).toBe('My Integration Test List');

      // Act 2: Add an item to the new list.
      const addItemResponse = await request
        .post(`/api/users/shopping-lists/${newList.shopping_list_id}/items`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ customItemName: 'Custom Test Item' });
      const addedItem = addItemResponse.body.data;

      // Assert 2: Check that the item was added.
      expect(addItemResponse.status).toBe(201);
      expect(addedItem.custom_item_name).toBe('Custom Test Item');

      // Assert 3: Fetch all lists and verify the new item is present in the correct list.
      const fetchResponse = await request
        .get('/api/users/shopping-lists')
        .set('Authorization', `Bearer ${authToken}`);
      const lists = fetchResponse.body.data;
      expect(fetchResponse.status).toBe(200);
      const updatedList = lists.find(
        (l: ShoppingList) => l.shopping_list_id === newList.shopping_list_id,
      );
      // The `add` endpoint returns the new ID as a string, while the `fetch` endpoint returns it as a number.
      // To make the test robust, we convert both to strings before comparing.
      expect(String(updatedList?.items[0].shopping_list_item_id)).toEqual(
        String(addedItem.shopping_list_item_id),
      );
    });
  });

  it('should allow a user to upload an avatar image and update their profile', async () => {
    // Arrange: Path to a dummy image file
    const dummyImagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');

    // Act: Make the POST request to upload the avatar
    const response = await request
      .post('/api/users/profile/avatar')
      .set('Authorization', `Bearer ${authToken}`)
      .attach('avatar', dummyImagePath);

    // Assert: Check the response
    expect(response.status).toBe(200);
    const updatedProfile = response.body.data;
    expect(updatedProfile.avatar_url).toBeDefined();
    expect(updatedProfile.avatar_url).not.toBeNull();
    expect(updatedProfile.avatar_url).toContain('/uploads/avatars/test-avatar');

    // Assert (Verification): Fetch the profile again to ensure the change was persisted
    const verifyResponse = await request
      .get('/api/users/profile')
      .set('Authorization', `Bearer ${authToken}`);
    const refetchedProfile = verifyResponse.body.data;
    expect(refetchedProfile.avatar_url).toBe(updatedProfile.avatar_url);
  });

  it('should reject avatar upload for an invalid file type', async () => {
    // Arrange: Create a buffer representing a text file.
    const invalidFileBuffer = Buffer.from('This is not an image file.');
    const invalidFileName = 'test.txt';

    // Act: Attempt to upload the text file to the avatar endpoint.
    const response = await request
      .post('/api/users/profile/avatar')
      .set('Authorization', `Bearer ${authToken}`)
      .attach('avatar', invalidFileBuffer, invalidFileName);

    // Assert: Check for a 400 Bad Request response.
    // This error comes from ValidationError via the global errorHandler (sendError format).
    expect(response.status).toBe(400);
    expect(response.body.error.message).toBe('Only image files are allowed!');
  });

  it('should reject avatar upload for a file that is too large', async () => {
    // Arrange: Create a buffer larger than the configured limit (e.g., > 1MB).
    // The limit is set in the multer middleware in `user.routes.ts`.
    // We'll create a 2MB buffer to be safe.
    const largeFileBuffer = Buffer.alloc(2 * 1024 * 1024, 'a');
    const largeFileName = 'large-avatar.jpg';

    // Act: Attempt to upload the large file.
    const response = await request
      .post('/api/users/profile/avatar')
      .set('Authorization', `Bearer ${authToken}`)
      .attach('avatar', largeFileBuffer, largeFileName);

    // Assert: Check for a 400 Bad Request response from the multer error handler.
    expect(response.status).toBe(400);
    expect(response.body.message).toBe('File upload error: File too large');
  });
});
