// src/pages/admin/components/ProfileManager.Authenticated.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, cleanup } 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';

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

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

// Define authenticated user data at the top level to be accessible by all describe blocks.
const authenticatedUser = { user_id: 'auth-user-123', email: 'test@example.com' };
const mockAddressId = 123;
const authenticatedProfile = {
  user_id: 'auth-user-123',
  full_name: 'Test User',
  avatar_url: 'http://example.com/avatar.png',
  role: 'user' as const,
  points: 100,
  preferences: {
    darkMode: false,
    unitSystem: 'imperial' as const,
  },
  address_id: mockAddressId, // Add address_id to ensure address fetching is triggered
};

const mockAddress = {
  address_id: mockAddressId,
  user_id: 'auth-user-123',
  address_line_1: '123 Main St',
  city: 'Anytown',
  province_state: 'ON',
  postal_code: 'A1B 2C3',
  country: 'Canada',
  latitude: 43.0,
  longitude: -79.0,
  created_at: new Date().toISOString(),
  updated_at: new Date().toISOString(),
};

const authenticatedProps = {
  isOpen: true,
  onClose: mockOnClose,
  user: authenticatedUser,
  authStatus: 'AUTHENTICATED' as const,
  profile: authenticatedProfile,
  onProfileUpdate: mockOnProfileUpdate,
  onSignOut: mockOnSignOut,
  onLoginSuccess: mockOnLoginSuccess,
};

// A helper function to set up all default successful API mocks.
const setupSuccessMocks = () => {
  // Mock getUserAddress to return a valid address when called
  (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);
  // Add a mock for geocodeAddress to prevent import errors and support address form functionality.
  (mockedApiClient.geocodeAddress as Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({ lat: 43.1, lng: -79.1 }) });
};

describe('ProfileManager Authenticated User Features', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    setupSuccessMocks();
  });

  afterEach(() => {
    cleanup();
  });

  // --- Authenticated View ---
  it('should render profile tabs when authStatus is AUTHENTICATED', () => {
    render(<ProfileManager {...authenticatedProps} />);

    expect(screen.getByRole('heading', { name: /my account/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /^profile$/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /security/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /data & privacy/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /preferences/i })).toBeInTheDocument();
    expect(screen.queryByRole('heading', { name: /^sign in$/i })).not.toBeInTheDocument();
  });

  // --- Profile Tab ---
  it('should allow updating the user profile and address', async () => {
    // Mock API calls for this specific test
    const updatedProfileData = { ...authenticatedProfile, full_name: 'Updated Name' };
    const updatedAddressData = { ...mockAddress, city: 'NewCity' };

    // Use manually-resolvable promises for updateUserProfile and updateUserAddress to control loading state
    let resolveProfileUpdate: (value: Response) => void = null!;
    const profileUpdatePromise = new Promise<Response>(resolve => { resolveProfileUpdate = resolve; });
    vi.mocked(mockedApiClient.updateUserProfile).mockReturnValue(profileUpdatePromise);

    let resolveAddressUpdate: (value: Response) => void = null!;
    const addressUpdatePromise = new Promise<Response>(resolve => { resolveAddressUpdate = resolve; });
    vi.mocked(mockedApiClient.updateUserAddress).mockReturnValue(addressUpdatePromise);

    render(<ProfileManager {...authenticatedProps} />);

    // Wait for initial data fetch (getUserAddress) to complete and component to render with initial values
    await waitFor(() => expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name));
    await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

    // Get elements after initial render
    const nameInput = screen.getByLabelText(/full name/i);
    const cityInput = screen.getByLabelText(/city/i);
    const saveButton = screen.getByRole('button', { name: /save profile/i });

    // Change inputs
    fireEvent.change(nameInput, { target: { value: 'Updated Name' } });
    fireEvent.change(cityInput, { target: { value: 'NewCity' } });

    // Assert that the button is not disabled before click
    expect(saveButton).not.toBeDisabled();

    fireEvent.click(saveButton);

    // Assert that the button is disabled immediately after click
    await waitFor(() => expect(saveButton).toBeDisabled());

    // Resolve the promises
    resolveProfileUpdate(new Response(JSON.stringify(updatedProfileData)));
    resolveAddressUpdate(new Response(JSON.stringify(updatedAddressData)));

    // Wait for the updates to complete and assertions
    await waitFor(() => {
      expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({ full_name: 'Updated Name', avatar_url: authenticatedProfile.avatar_url }, expect.objectContaining({ signal: expect.anything() }));
      expect(mockedApiClient.updateUserAddress).toHaveBeenCalledWith(expect.objectContaining({ ...mockAddress, city: 'NewCity' }), expect.objectContaining({ signal: expect.anything() }));
      expect(mockOnProfileUpdate).toHaveBeenCalledWith(expect.objectContaining({ full_name: 'Updated Name' }));
      expect(notifySuccess).toHaveBeenCalledWith(expect.stringMatching(/Profile.*updated/));
    });
  });

  it('should show an error if updating the profile fails', async () => {
    // Mock the failing promise. The useApi hook will catch this and throw an error.
    vi.mocked(mockedApiClient.updateUserProfile).mockRejectedValueOnce(new Error('Profile update failed'));

    // Mock the successful address update.
    vi.mocked(mockedApiClient.updateUserAddress).mockResolvedValueOnce(
      new Response(JSON.stringify(mockAddress), { status: 200 })
    );

    render(<ProfileManager {...authenticatedProps} />);
    // Wait for initial data fetch (getUserAddress) to complete
    await waitFor(() => expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name));

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

    // The useApi hook will show the notification.
    await waitFor(() => {
      expect(notifyError).toHaveBeenCalledWith('Profile update failed');
    });


    expect(mockOnProfileUpdate).not.toHaveBeenCalled();
    expect(mockOnClose).not.toHaveBeenCalled();
  });

  it('should show an error if updating the address fails', async () => {
    // Ensure profile update succeeds so we isolate the address update failure
    vi.mocked(mockedApiClient.updateUserProfile).mockResolvedValueOnce(
      new Response(JSON.stringify(authenticatedProfile), { status: 200 })
    );

    // Mock the failing promise for the address update.
    vi.mocked(mockedApiClient.updateUserAddress).mockRejectedValueOnce(new Error('Address update failed'));

    render(<ProfileManager {...authenticatedProps} />);
    // Wait for initial data fetch (getUserAddress) to complete
    await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));

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

    // Since only the address changed and it failed, we expect an error notification (handled by useApi)
    // and NOT a success message.
    await waitFor(() => {
      expect(notifyError).toHaveBeenCalledWith('Address update failed');
    });

    expect(notifySuccess).not.toHaveBeenCalled();
    expect(mockOnProfileUpdate).not.toHaveBeenCalled();
  });

  // --- Security Tab ---
  it('should allow updating the password', async () => {
    render(<ProfileManager {...authenticatedProps} />);
    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.objectContaining({ signal: expect.anything() }));
      expect(notifySuccess).toHaveBeenCalledWith('Password updated successfully!');
    });
  });

  it('should show an error if passwords do not match', async () => {
    render(<ProfileManager {...authenticatedProps} />);
    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();
  });

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

    render(<ProfileManager {...authenticatedProps} />);
    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 () => {
    const { unmount } = render(<ProfileManager {...authenticatedProps} />);
    fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));

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

    fireEvent.change(screen.getByPlaceholderText(/enter your password/i), { target: { value: 'correctpassword' } });
    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(mockedApiClient.deleteUserAccount).toHaveBeenCalledWith('correctpassword', expect.objectContaining({ signal: expect.anything() }));
      expect(notifySuccess).toHaveBeenCalledWith("Account deleted successfully. You will be logged out shortly.");
    });

    await waitFor(() => {
      expect(mockOnClose).toHaveBeenCalled();
      expect(mockOnSignOut).toHaveBeenCalled();
    }, { timeout: 3500 });

    unmount();
  });

  it('should show an error on account deletion with wrong password', async () => {
    (mockedApiClient.deleteUserAccount as Mock).mockResolvedValueOnce(
      new Response(JSON.stringify({ message: 'Incorrect password.' }), { status: 401 })
    );
    render(<ProfileManager {...authenticatedProps} />);
    fireEvent.click(screen.getByRole('button', { name: /data & privacy/i }));

    fireEvent.click(screen.getByRole('button', { name: /delete my account/i }));
    fireEvent.change(screen.getByPlaceholderText(/enter your password/i), { target: { value: 'wrongpassword' } });
    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('Incorrect password.');
    });
    expect(mockOnSignOut).not.toHaveBeenCalled();
  });

  // --- Preferences Tab ---
  it('should allow toggling dark mode', async () => {
    render(<ProfileManager {...authenticatedProps} />);
    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.objectContaining({ signal: expect.anything() }));
      expect(mockOnProfileUpdate).toHaveBeenCalledWith(
        expect.objectContaining({ preferences: expect.objectContaining({ darkMode: true }) })
      );
    });
  });

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

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

    expect(imperialRadio).toBeChecked();
    expect(metricRadio).not.toBeChecked();

    fireEvent.click(metricRadio);

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