// src/pages/admin/components/ProfileManager.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, cleanup, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { ProfileManager } from './ProfileManager';
import * as apiClient from '../../../services/apiClient';
import { notifySuccess, notifyError } from '../../../services/notificationService';
import toast from 'react-hot-toast';
import * as logger from '../../../services/logger.client';
import {
  createMockAddress,
  createMockUser,
  createMockUserProfile,
} from '../../../tests/utils/mockFactories';
import { QueryWrapper } from '../../../tests/utils/renderWithProviders';

// Unmock the component to test the real implementation
vi.unmock('./ProfileManager');

const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });

// Must explicitly call vi.mock() for apiClient
vi.mock('../../../services/apiClient');

vi.mock('../../../components/PasswordInput', () => ({
  // Mock the moved component
  PasswordInput: (props: any) => <input {...props} data-testid="password-input" />,
}));

const mockedApiClient = vi.mocked(apiClient, true);

const mockOnClose = vi.fn();
const mockOnLoginSuccess = vi.fn();
const mockOnSignOut = vi.fn();
const mockOnProfileUpdate = vi.fn();

// --- MOCK DATA ---
const authenticatedUser = createMockUser({ user_id: 'auth-user-123', email: 'test@example.com' });
const mockAddressId = 123;
const authenticatedProfile = createMockUserProfile({
  full_name: 'Test User',
  avatar_url: 'https://example.com/avatar.png',
  role: 'user',
  points: 100,
  preferences: {
    darkMode: false,
    unitSystem: 'imperial',
  },
  user: authenticatedUser,
  address_id: mockAddressId,
});

const mockAddress = createMockAddress({
  address_id: mockAddressId,
  address_line_1: '123 Main St',
  city: 'Anytown',
  province_state: 'ON',
  postal_code: 'A1B 2C3',
  country: 'Canada',
  latitude: 43.0,
  longitude: -79.0,
});

const defaultSignedOutProps = {
  isOpen: true,
  onClose: mockOnClose,
  authStatus: 'SIGNED_OUT' as const,
  userProfile: null,
  onProfileUpdate: mockOnProfileUpdate,
  onSignOut: mockOnSignOut,
  onLoginSuccess: mockOnLoginSuccess,
};

const defaultAuthenticatedProps = {
  isOpen: true,
  onClose: mockOnClose,
  authStatus: 'AUTHENTICATED' as const,
  userProfile: authenticatedProfile,
  onProfileUpdate: mockOnProfileUpdate,
  onSignOut: mockOnSignOut,
  onLoginSuccess: mockOnLoginSuccess,
};

const setupSuccessMocks = () => {
  const mockAuthResponse = { userprofile: authenticatedProfile, token: 'mock-token' };
  (mockedApiClient.loginUser as Mock).mockResolvedValue(
    new Response(JSON.stringify(mockAuthResponse)),
  );
  (mockedApiClient.registerUser as Mock).mockResolvedValue(
    new Response(JSON.stringify(mockAuthResponse)),
  );
  (mockedApiClient.requestPasswordReset as Mock).mockResolvedValue(
    new Response(JSON.stringify({ message: 'Password reset email sent.' })),
  );
  (mockedApiClient.getUserAddress as Mock).mockResolvedValue(
    new Response(JSON.stringify(mockAddress)),
  );
  (mockedApiClient.updateUserProfile as Mock).mockImplementation((data) =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ ...authenticatedProfile, ...(data as object) }),
    } as Response),
  );
  (mockedApiClient.updateUserPassword as Mock).mockResolvedValue(
    new Response(JSON.stringify({ message: 'Password updated successfully.' }), { status: 200 }),
  );
  (mockedApiClient.updateUserPreferences as Mock).mockImplementation((prefs) =>
    Promise.resolve({
      ok: true,
      json: () =>
        Promise.resolve({
          ...authenticatedProfile,
          preferences: { ...authenticatedProfile.preferences, ...prefs },
        }),
    } as Response),
  );
  (mockedApiClient.exportUserData as Mock).mockResolvedValue({
    ok: true,
    json: () =>
      Promise.resolve({ profile: authenticatedProfile, watchedItems: [], shoppingLists: [] }),
  } as Response);
  (mockedApiClient.deleteUserAccount as Mock).mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ message: 'Account deleted successfully.' }),
  } as Response);
  (mockedApiClient.geocodeAddress as Mock).mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ lat: 43.1, lng: -79.1 }),
  });
  (mockedApiClient.updateUserAddress as Mock).mockResolvedValue(
    new Response(JSON.stringify(mockAddress)),
  );
};

describe('ProfileManager', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    setupSuccessMocks();
    // Mock window.confirm for deletion tests
    vi.spyOn(window, 'confirm').mockReturnValue(true);
  });

  afterEach(() => {
    cleanup();
    // CRITICAL FIX: Reset timers after each test to prevent pollution that causes timeouts.
    vi.useRealTimers();
  });

  // =================================================================
  // == Authentication Flow Tests (Previously ProfileManager.Auth.test.tsx)
  // =================================================================
  describe('Authentication Flows (Signed Out)', () => {
    it('should render the Sign In form when authStatus is SIGNED_OUT', () => {
      renderWithQuery(<ProfileManager {...defaultSignedOutProps} />);
      expect(screen.getByRole('heading', { name: /^sign in$/i })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /register/i })).toBeInTheDocument();
    });

    it('should call loginUser and onLoginSuccess on successful login', async () => {
      renderWithQuery(<ProfileManager {...defaultSignedOutProps} />);
      fireEvent.change(screen.getByLabelText(/email address/i), {
        target: { value: 'user@test.com' },
      });
      fireEvent.change(screen.getByLabelText(/^password$/i), {
        target: { value: 'securepassword' },
      });
      fireEvent.submit(screen.getByTestId('auth-form'));

      await waitFor(() => {
        expect(mockedApiClient.loginUser).toHaveBeenCalledWith(
          'user@test.com',
          'securepassword',
          false,
        );
        expect(mockOnLoginSuccess).toHaveBeenCalledWith(authenticatedProfile, 'mock-token', false);
        expect(mockOnClose).toHaveBeenCalled();
      });
    });

    it('should switch to the Create an Account form and register successfully', async () => {
      renderWithQuery(<ProfileManager {...defaultSignedOutProps} />);
      fireEvent.click(screen.getByRole('button', { name: /register/i }));

      expect(screen.getByRole('heading', { name: /create an account/i })).toBeInTheDocument();

      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New User' } });
      fireEvent.change(screen.getByLabelText(/email address/i), {
        target: { value: 'new@test.com' },
      });
      fireEvent.change(screen.getByLabelText(/^password$/i), { target: { value: 'newpassword' } });
      fireEvent.submit(screen.getByTestId('auth-form'));

      await waitFor(() => {
        expect(mockedApiClient.registerUser).toHaveBeenCalledWith(
          'new@test.com',
          'newpassword',
          'New User',
          '',
        );
        expect(mockOnLoginSuccess).toHaveBeenCalled();
        expect(mockOnClose).toHaveBeenCalled();
      });
    });

    it('should switch to the Reset Password form and request a reset', async () => {
      renderWithQuery(<ProfileManager {...defaultSignedOutProps} />);
      fireEvent.click(screen.getByRole('button', { name: /forgot password/i }));

      expect(screen.getByRole('heading', { name: /reset password/i })).toBeInTheDocument();

      fireEvent.change(screen.getByLabelText(/email address/i), {
        target: { value: 'reset@test.com' },
      });
      fireEvent.submit(screen.getByTestId('reset-password-form'));

      await waitFor(() => {
        expect(mockedApiClient.requestPasswordReset).toHaveBeenCalledWith('reset@test.com');
        expect(notifySuccess).toHaveBeenCalledWith('Password reset email sent.');
      });
    });
  });

  // =================================================================
  // == Authenticated User Tests (Previously ProfileManager.Authenticated.test.tsx)
  // =================================================================
  describe('Authenticated User Features', () => {
    it('should render profile tabs when authStatus is AUTHENTICATED', () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      expect(screen.getByRole('heading', { name: /my account/i })).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /^profile$/i })).toBeInTheDocument();
      expect(screen.queryByRole('heading', { name: /^sign in$/i })).not.toBeInTheDocument();
    });

    it('should close the modal when clicking the backdrop', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      // The backdrop is the element with role="dialog"
      const backdrop = screen.getByRole('dialog');
      fireEvent.click(backdrop);

      await waitFor(() => {
        expect(mockOnClose).toHaveBeenCalled();
      });
    });

    it('should reset state when the modal is closed and reopened', async () => {
      const { rerender } = renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/full name/i)).toHaveValue('Test User'));

      // Change a value
      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'A New Name' } });
      expect(screen.getByLabelText(/full name/i)).toHaveValue('A New Name');

      // "Close" the modal
      rerender(<ProfileManager {...defaultAuthenticatedProps} isOpen={false} />);
      expect(screen.queryByRole('heading', { name: /my account/i })).not.toBeInTheDocument();

      // "Reopen" the modal
      rerender(<ProfileManager {...defaultAuthenticatedProps} isOpen={true} />);
      await waitFor(() => {
        // The name should be reset to the initial profile value, not 'A New Name'
        expect(screen.getByLabelText(/full name/i)).toHaveValue('Test User');
      });
    });

    it('should show an error if trying to save profile when not logged in', async () => {
      const loggerSpy = vi.spyOn(logger.logger, 'warn');
      // This is an edge case, but good to test the safeguard
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} userProfile={null} />);
      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'Updated Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Cannot save profile, no user is logged in.');
        expect(loggerSpy).toHaveBeenCalledWith(
          '[handleProfileSave] Aborted: No user is logged in.',
        );
      });
      expect(mockedApiClient.updateUserProfile).not.toHaveBeenCalled();
    });

    it('should show a notification if trying to save with no changes', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        expect(notifySuccess).toHaveBeenCalledWith('No changes to save.');
      });
      expect(mockedApiClient.updateUserProfile).not.toHaveBeenCalled();
      expect(mockedApiClient.updateUserAddress).not.toHaveBeenCalled();
      expect(mockOnClose).toHaveBeenCalled();
    });

    it('should handle failure when fetching user address', async () => {
      console.log('[TEST DEBUG] Running: should handle failure when fetching user address');
      const loggerSpy = vi.spyOn(logger.logger, 'warn');
      mockedApiClient.getUserAddress.mockRejectedValue(new Error('Address not found'));
      console.log('[TEST DEBUG] Mocked apiClient.getUserAddress to reject.');
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      await waitFor(() => {
        console.log(
          '[TEST DEBUG] Waiting for assertions. Current logger calls:',
          loggerSpy.mock.calls,
        );
        expect(notifyError).toHaveBeenCalledWith('Address not found');
        // The useProfileAddress hook logs a specific message when the fetch returns null (which useApi does on error)
        expect(loggerSpy).toHaveBeenCalledWith(
          `[useProfileAddress] Fetch returned null for addressId: ${mockAddressId}.`,
        );
      });
    });

    it('should handle partial success when saving profile and address', async () => {
      const loggerSpy = vi.spyOn(logger.logger, 'warn');
      // Mock profile update to succeed
      mockedApiClient.updateUserProfile.mockResolvedValue(
        new Response(JSON.stringify({ ...authenticatedProfile, full_name: 'New Name' })),
      );
      // Mock address update to fail (useApi will return null)
      mockedApiClient.updateUserAddress.mockRejectedValue(new Error('Address update failed'));

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      // Change both profile and address data
      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New Name' } });
      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'NewCity' } });

      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        // The useApi hook for the failed call will show its own error
        expect(notifyError).toHaveBeenCalledWith('Address update failed');
        // The profile update should still go through
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(
          expect.objectContaining({ full_name: 'New Name' }),
        );
        // The specific warning for partial failure should be logged
        expect(loggerSpy).toHaveBeenCalledWith(
          '[handleProfileSave] One or more operations failed. The mutation hook should have shown an error. The modal will remain open.',
        );
        // The modal should remain open and no global success message shown
        expect(mockOnClose).not.toHaveBeenCalled();
        expect(notifySuccess).not.toHaveBeenCalledWith('Profile updated successfully!');
      });
    });

    it('should handle unexpected critical error during profile save', async () => {
      const loggerSpy = vi.spyOn(logger.logger, 'warn');
      mockedApiClient.updateUserProfile.mockRejectedValue(new Error('Catastrophic failure'));
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        // The mutation's onError handler will notify with the error message.
        expect(notifyError).toHaveBeenCalledWith('Catastrophic failure');
        // A warning is logged about the partial failure
        expect(loggerSpy).toHaveBeenCalledWith(
          '[handleProfileSave] One or more operations failed. The mutation hook should have shown an error. The modal will remain open.',
        );
      });
    });

    it('should handle unexpected Promise.allSettled rejection during save', async () => {
      const allSettledSpy = vi
        .spyOn(Promise, 'allSettled')
        .mockRejectedValueOnce(new Error('AllSettled failed'));
      const loggerSpy = vi.spyOn(logger.logger, 'error');

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        expect(loggerSpy).toHaveBeenCalledWith(
          { err: new Error('AllSettled failed') },
          "[CRITICAL] An unexpected error was caught directly in handleProfileSave's catch block.",
        );
        expect(notifyError).toHaveBeenCalledWith(
          'An unexpected critical error occurred: AllSettled failed',
        );
      });

      allSettledSpy.mockRestore();
    });

    it('should show map view when address has coordinates', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => {
        expect(screen.getByTestId('map-view-container')).toBeInTheDocument();
      });
    });

    it('should not show map view when address has no coordinates', async () => {
      const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
      mockedApiClient.getUserAddress.mockResolvedValue(
        new Response(JSON.stringify(addressWithoutCoords)),
      );
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => {
        expect(screen.queryByTestId('map-view-container')).not.toBeInTheDocument();
      });
    });

    it('should show error if geocoding is attempted with no address string', async () => {
      mockedApiClient.getUserAddress.mockResolvedValue(new Response(JSON.stringify({})));
      renderWithQuery(
        <ProfileManager
          {...defaultAuthenticatedProps}
          userProfile={{ ...authenticatedProfile, address_id: 999 }}
        />,
      );

      await waitFor(() => {
        // Wait for initial render to settle
        expect(screen.getByRole('button', { name: /re-geocode/i })).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));

      await waitFor(() => {
        expect(toast.error).toHaveBeenCalledWith(
          'Please fill in the address fields before geocoding.',
        );
      });
    });

    it('should automatically geocode address after user stops typing (using fake timers)', async () => {
      // This test verifies debounced auto-geocoding behavior.
      // We use real timers throughout but wait for the debounce naturally.
      vi.useRealTimers();
      const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
      mockedApiClient.getUserAddress.mockResolvedValue(
        new Response(JSON.stringify(addressWithoutCoords)),
      );

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      // Wait for initial async address load to complete.
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue('Anytown'));

      // Change address, geocode should not be called immediately
      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'NewCity' } });
      expect(mockedApiClient.geocodeAddress).not.toHaveBeenCalled();

      // Wait for the debounce (1500ms) plus some buffer for the geocode call.
      // The auto-geocode effect fires after the debounced address value updates.
      await waitFor(
        () => {
          expect(mockedApiClient.geocodeAddress).toHaveBeenCalledWith(
            expect.stringContaining('NewCity'),
          );
        },
        { timeout: 3000 },
      );
      expect(toast.success).toHaveBeenCalledWith('Address geocoded successfully!');
    });

    it('should not geocode if address already has coordinates (using fake timers)', async () => {
      // Use real timers for the initial async render and data fetch
      vi.useRealTimers();
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      console.log('[TEST LOG] Waiting for initial address load...');
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue('Anytown'));

      // Switch to fake timers to control the debounce check
      vi.useFakeTimers();

      // Advance timers past the debounce threshold. Nothing should happen.
      act(() => {
        vi.advanceTimersByTime(1600);
      });
      console.log('[TEST LOG] Wait complete. Verifying no geocode call.');

      // geocode should not have been called because the initial address had coordinates
      expect(mockedApiClient.geocodeAddress).not.toHaveBeenCalled();
    });

    it('should show an error when trying to link an account', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /link google account/i })).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole('button', { name: /link google account/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith(
          'Account linking with google is not yet implemented.',
        );
      });
    });

    it('should show an error when trying to link a GitHub account', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /link github account/i })).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole('button', { name: /link github account/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith(
          'Account linking with github is not yet implemented.',
        );
      });
    });

    it('should switch between all tabs correctly', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      // Initial state: Profile tab
      expect(screen.getByLabelText('Profile Form')).toBeInTheDocument();

      // Switch to Security
      fireEvent.click(screen.getByRole('button', { name: /security/i }));
      expect(await screen.findByLabelText('New Password')).toBeInTheDocument();

      // Switch to Data & Privacy
      fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));
      expect(await screen.findByRole('heading', { name: /export your data/i })).toBeInTheDocument();

      // Switch to Preferences
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));
      expect(await screen.findByRole('heading', { name: /theme/i })).toBeInTheDocument();

      // Switch back to Profile
      fireEvent.click(screen.getByRole('button', { name: /^profile$/i }));
      expect(await screen.findByLabelText('Profile Form')).toBeInTheDocument();
    });

    it('should show an error if password is too short', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      fireEvent.change(screen.getByLabelText('New Password'), { target: { value: 'short' } });
      fireEvent.change(screen.getByLabelText('Confirm New Password'), {
        target: { value: 'short' },
      });
      fireEvent.submit(screen.getByTestId('update-password-form'), {});

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Password must be at least 6 characters long.');
      });
      expect(mockedApiClient.updateUserPassword).not.toHaveBeenCalled();
    });

    it('should show an error if account deletion fails', async () => {
      mockedApiClient.deleteUserAccount.mockRejectedValue(new Error('Deletion failed'));
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));
      fireEvent.click(screen.getByRole('button', { name: /delete my account/i }));

      fireEvent.change(screen.getByTestId('password-input'), {
        target: { value: 'password' },
      });
      fireEvent.submit(screen.getByTestId('delete-account-form'));

      const confirmButton = await screen.findByRole('button', { name: /yes, delete my account/i });
      fireEvent.click(confirmButton);

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Deletion failed');
      });
      expect(mockOnSignOut).not.toHaveBeenCalled();
    });

    it('should handle toggling dark mode when profile preferences are initially null', async () => {
      const profileWithoutPrefs = { ...authenticatedProfile, preferences: null as any };
      const { rerender } = renderWithQuery(
        <ProfileManager {...defaultAuthenticatedProps} userProfile={profileWithoutPrefs} />,
      );

      console.log('[TEST LOG] Clicking preferences tab...');
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));

      // FIX: Wait for the tab content to render after the click.
      const darkModeToggle = await screen.findByLabelText(/dark mode/i);
      // Test the ?? false fallback
      expect(darkModeToggle).not.toBeChecked();

      // Mock the API response for the update
      const updatedProfileWithPrefs = {
        ...profileWithoutPrefs,
        preferences: { darkMode: true, unitSystem: 'imperial' as const },
      };
      mockedApiClient.updateUserPreferences.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(updatedProfileWithPrefs),
      } as Response);

      console.log('[TEST LOG] Clicking dark mode toggle...');
      fireEvent.click(darkModeToggle);

      await waitFor(() => {
        expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ darkMode: true });
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(updatedProfileWithPrefs);
      });

      // Rerender with the new profile to check the UI update
      rerender(
        <ProfileManager {...defaultAuthenticatedProps} userProfile={updatedProfileWithPrefs} />,
      );

      // Wait for the preferences tab to be visible again before checking the toggle
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));
      expect(await screen.findByLabelText(/dark mode/i)).toBeChecked();
    });

    it('should allow updating the user profile and address', async () => {
      const updatedProfileData = { ...authenticatedProfile, full_name: 'Updated Name' };
      const updatedAddressData = { ...mockAddress, city: 'NewCity' };

      mockedApiClient.updateUserProfile.mockResolvedValue(
        new Response(JSON.stringify(updatedProfileData)),
      );
      mockedApiClient.updateUserAddress.mockResolvedValue(
        new Response(JSON.stringify(updatedAddressData)),
      );

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      await waitFor(() =>
        expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name),
      );
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'Updated Name' } });
      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'NewCity' } });

      const saveButton = screen.getByRole('button', { name: /save profile/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
          full_name: 'Updated Name',
          avatar_url: authenticatedProfile.avatar_url,
        });
        expect(mockedApiClient.updateUserAddress).toHaveBeenCalledWith(
          expect.objectContaining({ city: 'NewCity' }),
        );
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(
          expect.objectContaining({ full_name: 'Updated Name' }),
        );
        expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
      });
    });

    it('should show an error if updating the address fails but profile succeeds', async () => {
      mockedApiClient.updateUserProfile.mockResolvedValueOnce(
        new Response(JSON.stringify(authenticatedProfile), { status: 200 }),
      );
      mockedApiClient.updateUserAddress.mockRejectedValueOnce(new Error('Address update failed'));

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      // Change both profile and address data
      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New Name' } });
      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'NewCity' } });

      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        // The useApi hook will show the error for the failed call
        expect(notifyError).toHaveBeenCalledWith('Address update failed');
      });

      // The success notification should NOT be called because one of the promises failed
      expect(notifySuccess).not.toHaveBeenCalled();
      // FIX: The component logic is now corrected to call onProfileUpdate on partial success.
      expect(mockOnProfileUpdate).toHaveBeenCalledWith(authenticatedProfile);
      // The modal should remain open
      expect(mockOnClose).not.toHaveBeenCalled();
    });

    it('should allow updating the password', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      fireEvent.change(screen.getByLabelText('New Password'), {
        target: { value: 'newpassword123' },
      });
      fireEvent.change(screen.getByLabelText('Confirm New Password'), {
        target: { value: 'newpassword123' },
      });
      fireEvent.submit(screen.getByTestId('update-password-form'), {});

      await waitFor(() => {
        expect(mockedApiClient.updateUserPassword).toHaveBeenCalledWith('newpassword123');
        expect(notifySuccess).toHaveBeenCalledWith('Password updated successfully!');
      });
    });

    it('should show an error if passwords do not match', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      fireEvent.change(screen.getByLabelText('New Password'), {
        target: { value: 'newpassword123' },
      });
      fireEvent.change(screen.getByLabelText('Confirm New Password'), {
        target: { value: 'mismatch' },
      });
      fireEvent.submit(screen.getByTestId('update-password-form'), {});

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Passwords do not match.');
      });
      expect(mockedApiClient.updateUserPassword).not.toHaveBeenCalled();
    });

    it('should trigger data export', async () => {
      const anchorClickSpy = vi
        .spyOn(HTMLAnchorElement.prototype, 'click')
        .mockImplementation(() => {});

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));

      fireEvent.click(screen.getByRole('button', { name: /export my data/i }));

      await waitFor(() => {
        expect(mockedApiClient.exportUserData).toHaveBeenCalled();
        expect(anchorClickSpy).toHaveBeenCalled();
      });

      anchorClickSpy.mockRestore();
    });

    it('should handle account deletion flow', async () => {
      // Use fake timers to control the setTimeout call for the entire test.
      vi.useFakeTimers();

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));

      // Open the confirmation section
      fireEvent.click(screen.getByRole('button', { name: /delete my account/i }));
      expect(
        screen.getByText(/to confirm, please enter your current password/i),
      ).toBeInTheDocument();

      // Fill password and submit to open modal
      fireEvent.change(screen.getByTestId('password-input'), {
        target: { value: 'correctpassword' },
      });
      fireEvent.submit(screen.getByTestId('delete-account-form'));

      // Confirm in the modal
      // Use getByRole since the modal appears synchronously after the form submit.
      const confirmButton = screen.getByRole('button', { name: /yes, delete my account/i });
      fireEvent.click(confirmButton);

      // The async deleteAccount call is now pending. We need to flush promises
      // and then advance the timers to run the subsequent setTimeout.
      // `runAllTimersAsync` will resolve pending promises and run timers recursively.
      await act(async () => {
        await vi.runAllTimersAsync();
      });

      // Now that all timers and promises have been flushed, we can check the final state.
      expect(mockedApiClient.deleteUserAccount).toHaveBeenCalled();
      expect(notifySuccess).toHaveBeenCalled();
      expect(mockOnClose).toHaveBeenCalled();
      expect(mockOnSignOut).toHaveBeenCalled();
    });

    it('should allow toggling dark mode', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));

      const darkModeToggle = screen.getByLabelText(/dark mode/i);
      expect(darkModeToggle).not.toBeChecked();

      fireEvent.click(darkModeToggle);

      await waitFor(() => {
        expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({ darkMode: true });
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(
          expect.objectContaining({ preferences: expect.objectContaining({ darkMode: true }) }),
        );
      });
    });

    it('should allow changing the unit system', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));

      const metricRadio = screen.getByLabelText(/metric/i);
      fireEvent.click(metricRadio);

      await waitFor(() => {
        expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({
          unitSystem: 'metric',
        });
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(
          expect.objectContaining({
            preferences: expect.objectContaining({ unitSystem: 'metric' }),
          }),
        );
      });
    });

    it('should allow changing unit system when preferences are initially null', async () => {
      const profileWithoutPrefs = { ...authenticatedProfile, preferences: null as any };
      const { rerender } = renderWithQuery(
        <ProfileManager {...defaultAuthenticatedProps} userProfile={profileWithoutPrefs} />,
      );

      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));

      const imperialRadio = await screen.findByLabelText(/imperial/i);
      const metricRadio = screen.getByLabelText(/metric/i);

      // With null preferences, neither should be checked.
      expect(imperialRadio).not.toBeChecked();
      expect(metricRadio).not.toBeChecked();

      // Mock the API response for the update
      const updatedProfileWithPrefs = {
        ...profileWithoutPrefs,
        preferences: { darkMode: false, unitSystem: 'metric' as const },
      };
      mockedApiClient.updateUserPreferences.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(updatedProfileWithPrefs),
      } as Response);

      fireEvent.click(metricRadio);

      await waitFor(() => {
        expect(mockedApiClient.updateUserPreferences).toHaveBeenCalledWith({
          unitSystem: 'metric',
        });
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(updatedProfileWithPrefs);
      });

      // Rerender with the new profile to check the UI update
      rerender(
        <ProfileManager {...defaultAuthenticatedProps} userProfile={updatedProfileWithPrefs} />,
      );

      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));
      expect(await screen.findByLabelText(/metric/i)).toBeChecked();
      expect(screen.getByLabelText(/imperial/i)).not.toBeChecked();
    });

    it('should not call onProfileUpdate if updating unit system fails', async () => {
      mockedApiClient.updateUserPreferences.mockRejectedValue(new Error('API failed'));
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      fireEvent.click(screen.getByRole('button', { name: /preferences/i }));
      const metricRadio = await screen.findByLabelText(/metric/i);
      fireEvent.click(metricRadio);
      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('API failed');
      });
      expect(mockOnProfileUpdate).not.toHaveBeenCalled();
    });

    it('should only call updateProfile when only profile data has changed', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() =>
        expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name),
      );

      fireEvent.change(screen.getByLabelText(/full name/i), {
        target: { value: 'Only Name Changed' },
      });
      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        expect(mockedApiClient.updateUserProfile).toHaveBeenCalled();
        expect(mockedApiClient.updateUserAddress).not.toHaveBeenCalled();
        expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
      });
    });

    it('should only call updateAddress when only address data has changed', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'Only City Changed' } });
      fireEvent.click(screen.getByRole('button', { name: /save profile/i }));

      await waitFor(() => {
        expect(mockedApiClient.updateUserAddress).toHaveBeenCalled();
        expect(mockedApiClient.updateUserProfile).not.toHaveBeenCalled();
        expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
      });
    });

    it('should handle manual geocode success via button click', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      // Mock geocode response for the manual trigger
      (mockedApiClient.geocodeAddress as Mock).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ lat: 50.0, lng: -80.0 }),
      });

      fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));

      await waitFor(() => {
        expect(mockedApiClient.geocodeAddress).toHaveBeenCalled();
        expect(toast.success).toHaveBeenCalledWith('Address re-geocoded successfully!');
      });
    });

    it('should reset address form if profile has no address_id', async () => {
      const profileNoAddress = { ...authenticatedProfile, address_id: null };
      renderWithQuery(
        <ProfileManager {...defaultAuthenticatedProps} userProfile={profileNoAddress as any} />,
      );

      await waitFor(() => {
        // Address fields should be empty
        expect(screen.getByLabelText(/address line 1/i)).toHaveValue('');
        // Should not attempt to fetch address
        expect(mockedApiClient.getUserAddress).not.toHaveBeenCalled();
      });
    });

    it('should not render auth views when the user is already authenticated', () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      expect(screen.queryByText('Sign In')).not.toBeInTheDocument();
      expect(screen.queryByText('Create an Account')).not.toBeInTheDocument();
    });

    it('should log warning if address fetch returns null', async () => {
      console.log('[TEST DEBUG] Running: should log warning if address fetch returns null');
      const loggerSpy = vi.spyOn(logger.logger, 'warn');
      // Mock getUserAddress to return a successful response with a null body,
      // which useApi will parse to null.
      (mockedApiClient.getUserAddress as Mock).mockResolvedValue(
        new Response(JSON.stringify(null)),
      );
      console.log('[TEST DEBUG] Mocked apiClient.getUserAddress to resolve with a null body.');

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      await waitFor(() => {
        console.log(
          '[TEST DEBUG] Waiting for assertions. Current logger calls:',
          loggerSpy.mock.calls,
        );
        expect(loggerSpy).toHaveBeenCalledWith(
          `[useProfileAddress] Fetch returned null for addressId: ${mockAddressId}.`,
        );
      });
    });

    it('should handle updating the user profile and address with empty strings', async () => {
      mockedApiClient.updateUserProfile.mockImplementation(
        async (data) => new Response(JSON.stringify({ ...authenticatedProfile, ...data })),
      );
      mockedApiClient.updateUserAddress.mockImplementation(
        async (data) => new Response(JSON.stringify({ ...mockAddress, ...data })),
      );

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      await waitFor(() => {
        expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name);
      });
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: '' } });
      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: '' } });

      const saveButton = screen.getByRole('button', { name: /save profile/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
          full_name: '',
          avatar_url: authenticatedProfile.avatar_url,
        });
        expect(mockedApiClient.updateUserAddress).toHaveBeenCalledWith(
          expect.objectContaining({ city: '' }),
        );
        expect(mockOnProfileUpdate).toHaveBeenCalledWith(
          expect.objectContaining({ full_name: '' }),
        );
        expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
      });
    });

    it('should correctly clear the form when userProfile.address_id is null', async () => {
      const profileNoAddress = { ...authenticatedProfile, address_id: null };
      renderWithQuery(
        <ProfileManager
          {...defaultAuthenticatedProps}
          userProfile={profileNoAddress as any} // Forcefully override the type to simulate address_id: null
        />,
      );

      await waitFor(() => {
        expect(screen.getByLabelText(/address line 1/i)).toHaveValue('');
        expect(screen.getByLabelText(/city/i)).toHaveValue('');
        expect(screen.getByLabelText(/province \/ state/i)).toHaveValue('');
        expect(screen.getByLabelText(/postal \/ zip code/i)).toHaveValue('');
        expect(screen.getByLabelText(/country/i)).toHaveValue('');
      });
    });

    it('should show error notification when manual geocoding fails', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      (mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Geocoding failed'));

      fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Geocoding failed');
      });
    });

    it('should show error notification when auto-geocoding fails', async () => {
      vi.useFakeTimers();
      // FIX: Mock getUserAddress to return an address *without* coordinates.
      // This is the condition required to trigger the auto-geocoding logic.
      const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
      mockedApiClient.getUserAddress.mockResolvedValue(
        new Response(JSON.stringify(addressWithoutCoords)),
      );

      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);

      // Wait for initial load
      await act(async () => {
        await vi.runAllTimersAsync();
      });

      (mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Auto-geocode error'));

      fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'ErrorCity' } });

      await act(async () => {
        await vi.runAllTimersAsync();
      });

      expect(notifyError).toHaveBeenCalledWith('Auto-geocode error');
    });

    it('should handle permission denied error during geocoding', async () => {
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} />);
      await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

      (mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Permission denied'));

      fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));

      await waitFor(() => {
        expect(notifyError).toHaveBeenCalledWith('Permission denied');
      });
    });

    it('should not trigger OAuth link if user profile is missing', async () => {
      // This is an edge case to test the guard clause in handleOAuthLink
      renderWithQuery(<ProfileManager {...defaultAuthenticatedProps} userProfile={null} />);
      fireEvent.click(screen.getByRole('button', { name: /security/i }));

      const linkButton = await screen.findByRole('button', { name: /link google account/i });
      fireEvent.click(linkButton);

      // The function should just return, so nothing should happen.
      await waitFor(() => {
        expect(notifyError).not.toHaveBeenCalled();
      });
    });
  });
});
