// src/pages/UserProfilePage.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import UserProfilePage from './UserProfilePage';
import * as apiClient from '../services/apiClient';
import { UserProfile, Achievement, UserAchievement } from '../types';
import {
  createMockUserProfile,
  createMockUserAchievement,
  createMockUser,
} from '../tests/utils/mockFactories';

// Mock dependencies
vi.mock('../services/apiClient'); // This was correct
vi.mock('../services/logger.client', () => ({
  logger: {
    info: vi.fn(),
    error: vi.fn(),
  },
}));
vi.mock('../services/notificationService');
vi.mock('../services/aiApiClient'); // Mock aiApiClient as it's used in the component
const mockedNotificationService = vi.mocked(await import('../services/notificationService'));
vi.mock('../components/AchievementsList', () => ({
  AchievementsList: ({ achievements }: { achievements: (UserAchievement & Achievement)[] }) => (
    <div data-testid="achievements-list-mock">Achievements Count: {achievements.length}</div>
  ),
}));

const mockedApiClient = apiClient as Mocked<typeof apiClient>;

// --- Mock Data ---
const mockProfile: UserProfile = createMockUserProfile({
  user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }),
  full_name: 'Test User',
  avatar_url: 'http://example.com/avatar.jpg',
  points: 150,
  role: 'user',
});

const mockAchievements: (UserAchievement & Achievement)[] = [
  createMockUserAchievement({
    achievement_id: 1,
    user_id: 'user-123',
    achieved_at: '2024-01-01T00:00:00Z',
    name: 'First Steps',
    description: 'Uploaded first flyer.',
    icon: 'upload',
    points_value: 10,
  }),
];

describe('UserProfilePage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  // ... (Keep existing tests for loading message, error handling, rendering, etc.) ...

  it('should display a loading message initially', () => {
    mockedApiClient.getAuthenticatedUserProfile.mockReturnValue(new Promise(() => {}));
    mockedApiClient.getUserAchievements.mockReturnValue(new Promise(() => {}));
    render(<UserProfilePage />);
    expect(screen.getByText('Loading profile...')).toBeInTheDocument();
  });

  it('should display an error message if fetching profile fails', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Network Error'));
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    await waitFor(() => {
      expect(screen.getByText('Error: Network Error')).toBeInTheDocument();
    });
  });

  it('should display an error message if fetching profile returns a non-ok response', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify({ message: 'Auth Failed' }), { status: 401 }),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    await waitFor(() => {
      // The component throws 'Failed to fetch user profile.' because it just checks `!profileRes.ok`
      expect(screen.getByText('Error: Failed to fetch user profile.')).toBeInTheDocument();
    });
  });

  it('should display an error message if fetching achievements returns a non-ok response', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(mockProfile)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify({ message: 'Server Busy' }), { status: 503 }),
    );
    render(<UserProfilePage />);

    await waitFor(() => {
      // The component throws 'Failed to fetch user achievements.'
      expect(screen.getByText('Error: Failed to fetch user achievements.')).toBeInTheDocument();
    });
  });

  it('should display an error message if fetching achievements fails', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(mockProfile)),
    );
    mockedApiClient.getUserAchievements.mockRejectedValue(new Error('Achievements service down'));
    render(<UserProfilePage />);

    await waitFor(() => {
      expect(screen.getByText('Error: Achievements service down')).toBeInTheDocument();
    });
  });

  it('should handle unknown errors during fetch', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue('Unknown error string');
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    await waitFor(() => {
      expect(screen.getByText('Error: An unknown error occurred.')).toBeInTheDocument();
    });
  });

  it('should render the profile and achievements on successful fetch', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(mockProfile)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
      expect(screen.getByText('test@example.com')).toBeInTheDocument();
      expect(screen.getByText('150 Points')).toBeInTheDocument();
      expect(screen.getByAltText('User Avatar')).toHaveAttribute('src', mockProfile.avatar_url);
      expect(screen.getByTestId('achievements-list-mock')).toHaveTextContent(
        'Achievements Count: 1',
      );
    });
  });

  it('should render a fallback message if profile is null after loading', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(null)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    expect(await screen.findByText('Could not load user profile.')).toBeInTheDocument();
  });

  it('should display a fallback avatar if the user has no avatar_url', async () => {
    // Create a mock profile with a null avatar_url and a specific name for the seed
    const profileWithoutAvatar = { ...mockProfile, avatar_url: null, full_name: 'No Avatar User' };
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(profileWithoutAvatar)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(new Response(JSON.stringify([])));

    render(<UserProfilePage />);

    // Wait for the component to render with the fetched data
    await waitFor(() => {
      const avatarImage = screen.getByAltText('User Avatar');
      // JSDOM might not URL-encode spaces in the src attribute in the same way a browser does.
      // We adjust the expectation to match the literal string returned by getAttribute.
      const expectedSrc = 'https://api.dicebear.com/8.x/initials/svg?seed=No Avatar User';
      console.log('[TEST LOG] Actual Avatar Src:', avatarImage.getAttribute('src'));
      expect(avatarImage).toHaveAttribute('src', expectedSrc);
    });
  });

  it('should use email for avatar seed if full_name is missing', async () => {
    const profileNoName = { ...mockProfile, full_name: null, avatar_url: null };
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(profileNoName)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );

    render(<UserProfilePage />);

    await waitFor(() => {
      const avatar = screen.getByAltText('User Avatar');
      // seed should be the email
      expect(avatar.getAttribute('src')).toContain(`seed=${profileNoName.user.email}`);
    });
  });

  it('should trigger file input click when avatar is clicked', async () => {
    mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
      new Response(JSON.stringify(mockProfile)),
    );
    mockedApiClient.getUserAchievements.mockResolvedValue(
      new Response(JSON.stringify(mockAchievements)),
    );
    render(<UserProfilePage />);

    await screen.findByAltText('User Avatar');

    const fileInput = screen.getByTestId('avatar-file-input');
    const clickSpy = vi.spyOn(fileInput, 'click');

    const avatarContainer = screen.getByAltText('User Avatar');
    fireEvent.click(avatarContainer);

    expect(clickSpy).toHaveBeenCalled();
  });

  describe('Name Editing', () => {
    beforeEach(() => {
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
        new Response(JSON.stringify(mockProfile)),
      );
      mockedApiClient.getUserAchievements.mockResolvedValue(
        new Response(JSON.stringify(mockAchievements)),
      );
    });

    it('should allow editing and saving the user name', async () => {
      const updatedProfile = { ...mockProfile, full_name: 'Updated Name' };
      mockedApiClient.updateUserProfile.mockResolvedValue(
        new Response(JSON.stringify(updatedProfile)),
      );
      render(<UserProfilePage />);

      await screen.findByText('Test User');

      fireEvent.click(screen.getByRole('button', { name: /edit/i }));
      const nameInput = screen.getByRole('textbox');
      fireEvent.change(nameInput, { target: { value: 'Updated Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save/i }));

      await waitFor(() => {
        expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
          full_name: 'Updated Name',
        });
        expect(screen.getByRole('heading', { name: 'Updated Name' })).toBeInTheDocument();
      });
    });

    it('should allow canceling the name edit', async () => {
      render(<UserProfilePage />);
      await screen.findByText('Test User');

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

      expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
      expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
    });

    it('should show an error if saving the name fails with a non-ok response', async () => {
      mockedApiClient.updateUserProfile.mockResolvedValue(
        new Response(JSON.stringify({ message: 'Validation failed' }), { status: 400 }),
      );
      render(<UserProfilePage />);
      await screen.findByText('Test User');

      fireEvent.click(screen.getByRole('button', { name: /edit/i }));
      const nameInput = screen.getByRole('textbox');
      fireEvent.change(nameInput, { target: { value: 'Invalid Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save/i }));

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

    it('should show a default error if saving the name fails with a non-ok response and no message', async () => {
      mockedApiClient.updateUserProfile.mockResolvedValue(
        new Response(JSON.stringify({}), { status: 400 }),
      );
      render(<UserProfilePage />);
      await screen.findByText('Test User');

      fireEvent.click(screen.getByRole('button', { name: /edit/i }));
      const nameInput = screen.getByRole('textbox');
      fireEvent.change(nameInput, { target: { value: 'Invalid Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save/i }));

      await waitFor(() => {
        // This covers the `|| 'Failed to update name.'` part of the error throw
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
          'Failed to update name.',
        );
      });
    });

    it('should handle unknown errors when saving name', async () => {
      mockedApiClient.updateUserProfile.mockRejectedValue('Unknown update error');
      render(<UserProfilePage />);
      await screen.findByText('Test User');

      fireEvent.click(screen.getByRole('button', { name: /edit/i }));
      const nameInput = screen.getByRole('textbox');
      fireEvent.change(nameInput, { target: { value: 'New Name' } });
      fireEvent.click(screen.getByRole('button', { name: /save/i }));

      await waitFor(() => {
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
          'An unknown error occurred.',
        );
      });
    });
  });

  describe('Avatar Upload', () => {
    beforeEach(() => {
      mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
        new Response(JSON.stringify(mockProfile)),
      );
      mockedApiClient.getUserAchievements.mockResolvedValue(
        new Response(JSON.stringify(mockAchievements)),
      );
    });

    it('should upload a new avatar and update the image source', async () => {
      const updatedProfile = { ...mockProfile, avatar_url: 'http://example.com/new-avatar.png' };

      // Log when the mock is called
      mockedApiClient.uploadAvatar.mockImplementation((file) => {
        console.log('[TEST LOG] uploadAvatar mock called with:', file.name);
        // Add a slight delay to ensure "isUploading" state can be observed
        return new Promise((resolve) => {
          setTimeout(() => {
            console.log('[TEST LOG] uploadAvatar mock resolving...');
            resolve(new Response(JSON.stringify(updatedProfile)));
          }, 100);
        });
      });

      render(<UserProfilePage />);

      await screen.findByAltText('User Avatar');

      // Mock the hidden file input
      const fileInput = screen.getByTestId('avatar-file-input');
      const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });

      console.log('[TEST LOG] Firing file change event...');
      fireEvent.change(fileInput, { target: { files: [file] } });

      // DEBUG: Print current DOM state if spinner is not found immediately
      // const spinner = screen.queryByTestId('avatar-upload-spinner');
      // if (!spinner) {
      //   console.log('[TEST LOG] Spinner NOT found immediately after event.');
      //   // screen.debug(); // Uncomment to see DOM
      // } else {
      //   console.log('[TEST LOG] Spinner FOUND immediately.');
      // }

      // Wait for the spinner to appear
      console.log('[TEST LOG] Waiting for spinner...');
      await screen.findByTestId('avatar-upload-spinner');
      console.log('[TEST LOG] Spinner found.');

      // Wait for the upload to complete and the UI to update.
      await waitFor(() => {
        expect(mockedApiClient.uploadAvatar).toHaveBeenCalledWith(file);
        expect(screen.getByAltText('User Avatar')).toHaveAttribute(
          'src',
          updatedProfile.avatar_url,
        );
        expect(screen.queryByTestId('avatar-upload-spinner')).not.toBeInTheDocument();
      });
    });

    it('should not attempt to upload if no file is selected', async () => {
      render(<UserProfilePage />);
      await screen.findByAltText('User Avatar');

      const fileInput = screen.getByTestId('avatar-file-input');
      // Simulate user canceling the file dialog
      fireEvent.change(fileInput, { target: { files: null } });

      // Assert that no API call was made
      expect(mockedApiClient.uploadAvatar).not.toHaveBeenCalled();
    });

    it('should show an error if avatar upload returns a non-ok response', async () => {
      mockedApiClient.uploadAvatar.mockResolvedValue(
        new Response(JSON.stringify({ message: 'File too large' }), { status: 413 }),
      );
      render(<UserProfilePage />);
      await screen.findByAltText('User Avatar');

      const fileInput = screen.getByTestId('avatar-file-input');
      const file = new File(['(⌐□_□)'], 'large.png', { type: 'image/png' });
      fireEvent.change(fileInput, { target: { files: [file] } });

      await waitFor(() => {
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith('File too large');
      });
    });

    it('should show a default error if avatar upload returns a non-ok response and no message', async () => {
      mockedApiClient.uploadAvatar.mockResolvedValue(
        new Response(JSON.stringify({}), { status: 413 }),
      );
      render(<UserProfilePage />);
      await screen.findByAltText('User Avatar');

      const fileInput = screen.getByTestId('avatar-file-input');
      const file = new File(['(⌐□_□)'], 'large.png', { type: 'image/png' });
      fireEvent.change(fileInput, { target: { files: [file] } });

      await waitFor(() => {
        // This covers the `|| 'Failed to upload avatar.'` part of the error throw
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
          'Failed to upload avatar.',
        );
      });
    });

    it('should handle unknown errors when uploading avatar', async () => {
      mockedApiClient.uploadAvatar.mockRejectedValue('Unknown upload error');
      render(<UserProfilePage />);
      await screen.findByAltText('User Avatar');

      const fileInput = screen.getByTestId('avatar-file-input');
      const file = new File(['(⌐□_□)'], 'error.png', { type: 'image/png' });
      fireEvent.change(fileInput, { target: { files: [file] } });

      await waitFor(() => {
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
          'An unknown error occurred.',
        );
      });
    });

    it('should show an error if a non-image file is selected for upload', async () => {
      // Mock the API client to return a non-OK response, simulating server-side validation failure
      mockedApiClient.uploadAvatar.mockResolvedValue(
        new Response(
          JSON.stringify({
            message: 'Invalid file type. Only images (png, jpeg, gif) are allowed.',
          }),
          { status: 400, headers: { 'Content-Type': 'application/json' } },
        ),
      );

      render(<UserProfilePage />);
      await screen.findByAltText('User Avatar');

      const fileInput = screen.getByTestId('avatar-file-input');
      // Create a mock file that is NOT an image (e.g., a PDF)
      const nonImageFile = new File(['some text content'], 'document.pdf', {
        type: 'application/pdf',
      });

      fireEvent.change(fileInput, { target: { files: [nonImageFile] } });

      await waitFor(() => {
        expect(mockedApiClient.uploadAvatar).toHaveBeenCalledWith(nonImageFile);
        expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
          'Invalid file type. Only images (png, jpeg, gif) are allowed.',
        );
        expect(screen.queryByTestId('avatar-upload-spinner')).not.toBeInTheDocument();
      });
    });
  });
});
