// src/pages/admin/ActivityLog.test.tsx
import React from 'react';
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ActivityLog } from './ActivityLog';
import * as apiClient from '../../services/apiClient';
import type { ActivityLogItem, UserProfile } from '../../types';
import { createMockActivityLogItem, createMockUserProfile } from '../../tests/utils/mockFactories';

// The apiClient and logger are now mocked globally via src/tests/setup/tests-setup-unit.ts.
// We can cast it to its mocked type to get type safety and autocompletion.
const mockedApiClient = vi.mocked(apiClient);

// Mock date-fns to return a consistent value for snapshots
vi.mock('date-fns', () => {
  return {
    // Only mock the specific function used in the component.
    // This avoids potential issues with `importOriginal` in complex mocking scenarios.
    formatDistanceToNow: vi.fn(() => 'about 5 hours ago'),
  };
});

const mockUserProfile: UserProfile = createMockUserProfile({
  user: { user_id: 'user-123', email: 'test@example.com' },
});

const mockLogs: ActivityLogItem[] = [
  createMockActivityLogItem({
    activity_log_id: 1,
    user_id: 'user-123',
    action: 'flyer_processed',
    display_text: 'Processed a new flyer for Walmart.',
    user_avatar_url: 'http://example.com/avatar.png',
    user_full_name: 'Test User',
    details: { flyer_id: 1, store_name: 'Walmart' },
  }),
  createMockActivityLogItem({
    activity_log_id: 2,
    user_id: 'user-456',
    action: 'recipe_created',
    display_text: 'Jane Doe added a new recipe: Pasta Carbonara',
    user_full_name: 'Jane Doe',
    details: { recipe_id: 1, recipe_name: 'Pasta Carbonara' },
  }),
  createMockActivityLogItem({
    activity_log_id: 3,
    user_id: 'user-789',
    action: 'list_shared',
    display_text: 'John Smith shared a list.',
    user_full_name: 'John Smith',
    details: { list_name: 'Weekly Groceries', shopping_list_id: 10, shared_with_name: 'Test User' },
  }),
  createMockActivityLogItem({
    activity_log_id: 4,
    user_id: 'user-101',
    action: 'user_registered',
    display_text: 'New user joined',
    details: { full_name: 'Newbie User' }, // No avatar provided to test fallback
  }),
  createMockActivityLogItem({
    activity_log_id: 5,
    user_id: 'user-102',
    action: 'recipe_favorited',
    display_text: 'User favorited a recipe',
    user_full_name: 'Pizza Lover',
    user_avatar_url: 'http://example.com/pizza.png',
    details: { recipe_name: 'Best Pizza' },
  }),
  createMockActivityLogItem({
    activity_log_id: 6,
    user_id: 'user-103',
    action: 'unknown_action' as any, // Force unknown action to test default case
    display_text: 'Something happened',
    details: {} as any,
  }),
];

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

  it('should not render if userProfile is null', () => {
    const { container } = render(<ActivityLog userProfile={null} onLogClick={vi.fn()} />);
    expect(container).toBeEmptyDOMElement();
  });

  it('should show a loading state initially', async () => {
    let resolvePromise: (value: Response) => void;
    const mockPromise = new Promise<Response>((resolve) => {
      resolvePromise = resolve;
    });
    // Cast to any to bypass strict type checking for the mock return value vs Promise
    mockedApiClient.fetchActivityLog.mockReturnValue(mockPromise as any);

    render(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);

    expect(screen.getByText('Loading activity...')).toBeInTheDocument();

    await act(async () => {
      resolvePromise!(new Response(JSON.stringify([])));
    });
  });

  it('should display an error message if fetching logs fails', async () => {
    mockedApiClient.fetchActivityLog.mockRejectedValue(new Error('API is down'));
    render(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);
    await waitFor(() => {
      expect(screen.getByText('API is down')).toBeInTheDocument();
    });
  });

  it('should display a message when there are no logs', async () => {
    mockedApiClient.fetchActivityLog.mockResolvedValue(new Response(JSON.stringify([])));
    render(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);
    await waitFor(() => {
      expect(screen.getByText('No recent activity to show.')).toBeInTheDocument();
    });
  });

  it('should render a list of activities successfully covering all types', async () => {
    mockedApiClient.fetchActivityLog.mockResolvedValue(new Response(JSON.stringify(mockLogs)));
    render(<ActivityLog userProfile={mockUserProfile} />);
    await waitFor(() => {
      // Check for specific text from different log types
      expect(screen.getByText('Walmart')).toBeInTheDocument(); // From flyer_processed
      expect(screen.getByText('Pasta Carbonara')).toBeInTheDocument(); // From recipe_created
      expect(screen.getByText('Weekly Groceries')).toBeInTheDocument(); // From list_shared
      expect(screen.getByText('Newbie User')).toBeInTheDocument(); // From user_registered
      expect(screen.getByText('Best Pizza')).toBeInTheDocument(); // From recipe_favorited
      expect(screen.getByText('An unknown activity occurred.')).toBeInTheDocument(); // From unknown_action

      // Check for user names
      expect(screen.getByText('Jane Doe', { exact: false })).toBeInTheDocument();

      // Check for avatar
      const avatar = screen.getByAltText('Test User');
      expect(avatar).toBeInTheDocument();
      expect(avatar).toHaveAttribute('src', 'http://example.com/avatar.png');

      // Check for fallback avatar (Newbie User has no avatar)
      // The fallback is an SVG inside a span. We can check for the span's class or the SVG.
      // The container for fallback has specific classes.
      // We can look for the container associated with the "Newbie User" item.
      const newbieItem = screen.getByText('Newbie User').closest('li');
      const fallbackIcon = newbieItem?.querySelector('svg');
      expect(fallbackIcon).toBeInTheDocument();

      // Check for the mocked date
      expect(screen.getAllByText('about 5 hours ago')).toHaveLength(mockLogs.length);
    });
  });

  it('should call onLogClick when a clickable log item is clicked', async () => {
    const onLogClickMock = vi.fn();
    mockedApiClient.fetchActivityLog.mockResolvedValue(new Response(JSON.stringify(mockLogs)));
    render(<ActivityLog userProfile={mockUserProfile} onLogClick={onLogClickMock} />);

    await waitFor(() => {
      // Recipe Created
      const clickableRecipe = screen.getByText('Pasta Carbonara');
      fireEvent.click(clickableRecipe);
      expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[1]);

      // List Shared
      const clickableList = screen.getByText('Weekly Groceries');
      fireEvent.click(clickableList);
      expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[2]);

      // Recipe Favorited
      const clickableFav = screen.getByText('Best Pizza');
      fireEvent.click(clickableFav);
      expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[4]);
    });

    expect(onLogClickMock).toHaveBeenCalledTimes(3);
  });

  it('should not render clickable styling if onLogClick is undefined', async () => {
    mockedApiClient.fetchActivityLog.mockResolvedValue(new Response(JSON.stringify(mockLogs)));
    render(<ActivityLog userProfile={mockUserProfile} />); // onLogClick is undefined

    await waitFor(() => {
      const recipeName = screen.getByText('Pasta Carbonara');
      expect(recipeName).not.toHaveClass('cursor-pointer');
      expect(recipeName).not.toHaveClass('text-blue-500');

      const listName = screen.getByText('Weekly Groceries');
      expect(listName).not.toHaveClass('cursor-pointer');
    });
  });

  it('should handle missing details in logs gracefully (fallback values)', async () => {
    const logsWithMissingDetails: ActivityLogItem[] = [
      createMockActivityLogItem({
        activity_log_id: 101,
        user_id: 'u1',
        action: 'flyer_processed',
        display_text: '...',
        details: { flyer_id: 1, store_name: '' } as any, // Missing store_name, explicit empty to override mock default
      }),
      createMockActivityLogItem({
        activity_log_id: 102,
        user_id: 'u2',
        action: 'recipe_created',
        display_text: '...',
        details: { recipe_id: 1, recipe_name: '' } as any, // Missing recipe_name
      }),
      createMockActivityLogItem({
        activity_log_id: 103,
        user_id: 'u3',
        action: 'user_registered',
        display_text: '...',
        details: { full_name: '' } as any, // Missing full_name
      }),
      createMockActivityLogItem({
        activity_log_id: 104,
        user_id: 'u4',
        action: 'recipe_favorited',
        display_text: '...',
        details: { recipe_id: 2, recipe_name: '' } as any, // Missing recipe_name
      }),
      createMockActivityLogItem({
        activity_log_id: 105,
        user_id: 'u5',
        action: 'list_shared',
        display_text: '...',
        details: { shopping_list_id: 1, list_name: '', shared_with_name: '' } as any, // Missing list_name and shared_with_name
      }),
      createMockActivityLogItem({
        activity_log_id: 106,
        user_id: 'u6',
        action: 'flyer_processed',
        display_text: '...',
        user_avatar_url: 'http://img.com/a.png', // FIX: Moved from details
        user_full_name: '', // FIX: Moved from details to test fallback alt text
        details: { flyer_id: 2, store_name: 'Mock Store' } as any,
      }),
    ];

    mockedApiClient.fetchActivityLog.mockResolvedValue(
      new Response(JSON.stringify(logsWithMissingDetails)),
    );

    // Debug: verify structure of logs to ensure defaults are overridden
    console.log(
      'Testing fallback rendering with logs:',
      JSON.stringify(logsWithMissingDetails, null, 2),
    );

    const { container } = render(<ActivityLog userProfile={mockUserProfile} />);

    await waitFor(() => {
      console.log('[TEST DEBUG] Waiting for UI to update...');
      // Use screen.debug to log the current state of the DOM, which is invaluable for debugging.
      screen.debug(undefined, 30000);

      console.log('[TEST DEBUG] Checking for fallback text elements...');
      expect(screen.getAllByText('a store')[0]).toBeInTheDocument();
      expect(screen.getByText('Untitled Recipe')).toBeInTheDocument();
      expect(screen.getByText('A new user')).toBeInTheDocument();
      expect(screen.getByText('a recipe')).toBeInTheDocument();
      expect(screen.getByText('a shopping list')).toBeInTheDocument();
      expect(screen.getByText('another user')).toBeInTheDocument();
      console.log('[TEST DEBUG] All fallback text elements found!');

      console.log('[TEST DEBUG] Checking for avatar with fallback alt text...');
      // Check for empty alt text on avatar (item 106)
      const avatars = screen.getAllByRole('img');
      console.log(
        '[TEST DEBUG] Found avatars with alts:',
        avatars.map((img) => img.getAttribute('alt')),
      );
      const avatarWithFallbackAlt = avatars.find(
        (img) => img.getAttribute('alt') === 'User Avatar',
      );
      expect(avatarWithFallbackAlt).toBeInTheDocument();
      console.log('[TEST DEBUG] Fallback avatar with correct alt text found!');
    });
  });

  it('should display error message from API response when not OK', async () => {
    mockedApiClient.fetchActivityLog.mockResolvedValue(
      new Response(JSON.stringify({ message: 'Server says no' }), { status: 500 }),
    );
    render(<ActivityLog userProfile={mockUserProfile} />);
    await waitFor(() => {
      expect(screen.getByText('Server says no')).toBeInTheDocument();
    });
  });

  it('should display default error message from API response when not OK and no message provided', async () => {
    mockedApiClient.fetchActivityLog.mockResolvedValue(
      new Response(JSON.stringify({}), { status: 500 }),
    );
    render(<ActivityLog userProfile={mockUserProfile} />);
    await waitFor(() => {
      expect(screen.getByText('Failed to fetch logs')).toBeInTheDocument();
    });
  });

  it('should display generic error message when fetch throws non-Error object', async () => {
    mockedApiClient.fetchActivityLog.mockRejectedValue('String error');
    render(<ActivityLog userProfile={mockUserProfile} />);
    await waitFor(() => {
      expect(screen.getByText('Failed to load activity.')).toBeInTheDocument();
    });
  });
});
