// src/tests/integration/user.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as apiClient from '../../services/apiClient';
import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db';
import type { User, MasterGroceryItem, ShoppingList } from '../../types';

/**
 * @vitest-environment node
 */

const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$';

/**
 * A helper function to create a new user and log them in, returning the user object and auth token.
 * This provides an authenticated context for testing protected API endpoints.
 */
const createAndLoginUser = async (email: string) => {
  const password = TEST_PASSWORD;
  // Register the new user.
  const registerResponse = await apiClient.registerUser(email, password, 'Test User');
  if (!registerResponse.ok) {
    const errorData = await registerResponse.json();
    throw new Error(errorData.message || 'Test registration failed');
  }

  // Log in to get the auth token.
  const loginResponse = await apiClient.loginUser(email, password, false);
  const { user, token } = await loginResponse.json();
  return { user, token };
};

describe('User API Routes Integration Tests', () => {
  let testUser: User;
  let authToken: string;

  // --- START DEBUG LOGGING ---
  // Query the DB from within the test file to see its state.
  beforeAll(async () => {
    const res = await getPool().query('SELECT u.user_id, u.email, p.role FROM public.users u JOIN public.profiles p ON u.user_id = p.user_id');
    console.log('\n--- [user.integration.test.ts] Users found in DB from TEST perspective (beforeAll): ---');
    console.table(res.rows);
    console.log('-------------------------------------------------------------------------------------\n');
  });
  // --- END DEBUG LOGGING ---
  // 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 () => {
    const email = `user-test-${Date.now()}@example.com`;
    const { user, token } = await createAndLoginUser(email);
    testUser = user;
    authToken = token;
  });

  // 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 () => {
    const pool = getPool();
    try {
      // Find all users created during this test run by their email pattern.
      const res = await pool.query("SELECT user_id FROM public.users WHERE email LIKE 'user-test-%' OR email LIKE 'delete-me-%' OR email LIKE 'reset-me-%'");
      if (res.rows.length > 0) {
        const userIds = res.rows.map(r => r.user_id);
        logger.debug(`[user.integration.test.ts afterAll] Cleaning up ${userIds.length} test users...`);
        // Use a direct DB query for cleanup, which is faster and more reliable than API calls.
        await pool.query('DELETE FROM public.users WHERE user_id = ANY($1::uuid[])', [userIds]);
      }
    } catch (error) {
      logger.error('Failed to clean up test users from database.', { 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 apiClient.getAuthenticatedUserProfile(authToken);
    const profile = await response.json();

    // Assert: Verify the profile data matches the created user.
    expect(profile).toBeDefined();
    expect(profile.user_id).toBe(testUser.user_id);
    expect(profile.user.email).toBe(testUser.email);
    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 apiClient.updateUserProfile(profileUpdates, authToken);
    const updatedProfile = await response.json();

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

    // Also, fetch the profile again to ensure the change was persisted.
    const refetchResponse = await apiClient.getAuthenticatedUserProfile(authToken);
    const refetchedProfile = await refetchResponse.json();
    expect(refetchedProfile.full_name).toBe('Updated Test User');
  });

  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 apiClient.updateUserPreferences(preferenceUpdates, authToken);
    const updatedProfile = await response.json();

    // Assert: Check that the preferences object in the returned profile is updated.
    expect(updatedProfile).toBeDefined();
    expect(updatedProfile.preferences).toBeDefined();
    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 apiClient.registerUser(email, weakPassword, 'Weak Password User');
    expect(response.ok).toBe(false);
    const errorData = await response.json();
    expect(errorData.message).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 { token: deletionToken } = await createAndLoginUser(deletionEmail);

    // Act: Call the delete endpoint with the correct password and token.
    const response = await apiClient.deleteUserAccount(TEST_PASSWORD, deletionToken);
    const deleteResponse = await response.json();

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

    // Assert (Verification): Attempting to log in again with the same credentials should now fail.
    const loginResponse = await apiClient.loginUser(deletionEmail, TEST_PASSWORD, false);
    expect(loginResponse.ok).toBe(false);
    const errorData = await loginResponse.json();
    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 createResponse = await createAndLoginUser(resetEmail);
    const resetUser = createResponse.user;

    // Act 1: Request a password reset. In our test environment, the token is returned in the response.
    const resetRequestRawResponse = await apiClient.requestPasswordReset(resetEmail);
    if (!resetRequestRawResponse.ok) {
        const errorData = await resetRequestRawResponse.json();
        throw new Error(errorData.message || 'Password reset request failed');
    }
    const resetRequestResponse = await resetRequestRawResponse.json();
    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 apiClient.resetPassword(resetToken!, newPassword);
    if (!resetRawResponse.ok) {
        const errorData = await resetRawResponse.json();
        throw new Error(errorData.message || 'Password reset failed');
    }
    const resetResponse = await resetRawResponse.json();

    // 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 apiClient.loginUser(resetEmail, newPassword, false);
    const loginData = await loginResponse.json();
    expect(loginData.user).toBeDefined();
    expect(loginData.user.user_id).toBe(resetUser.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 apiClient.addWatchedItem('Integration Test Item', 'Other/Miscellaneous', authToken);
      const newItem = await addResponse.json();

      // Assert 1: Check that the item was created correctly.
      expect(newItem).toBeDefined();
      expect(newItem.name).toBe('Integration Test Item');

      // Act 2: Fetch all watched items for the user.
      const watchedItemsResponse = await apiClient.fetchWatchedItems(authToken);
      const watchedItems = await watchedItemsResponse.json();

      // 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.
      await apiClient.removeWatchedItem(newItem.master_grocery_item_id, authToken);

      // Assert 3: Fetch again and verify the item is gone.
      const finalWatchedItemsResponse = await apiClient.fetchWatchedItems(authToken);
      const finalWatchedItems = await finalWatchedItemsResponse.json();
      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 apiClient.createShoppingList('My Integration Test List', authToken);
      const newList = await createListResponse.json();

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

      // Act 2: Add an item to the new list.
      const addItemResponse = await apiClient.addShoppingListItem(newList.shopping_list_id, { customItemName: 'Custom Test Item' }, authToken);
      const addedItem = await addItemResponse.json();

      // Assert 2: Check that the item was added.
      expect(addedItem).toBeDefined();
      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 apiClient.fetchShoppingLists(authToken);
      const lists = await fetchResponse.json();
      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));
    });
  });
});