// src/App.test.tsx
import React from 'react';
import { render, screen, waitFor, fireEvent, within, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MemoryRouter, Outlet } from 'react-router-dom';
import App from './App';
import * as aiApiClient from './services/aiApiClient'; // Import aiApiClient
import * as apiClient from './services/apiClient';
import { AppProviders } from './providers/AppProviders';
import type { Flyer, Profile, User, UserProfile } from './types';
import type { HeaderProps } from './components/Header';

// Mock useAuth to allow overriding the user state in tests
const mockUseAuth = vi.fn();
vi.mock('./hooks/useAuth', () => ({
  useAuth: () => mockUseAuth(),
}));

// Mock the new data hooks
const mockUseFlyers = vi.fn();
vi.mock('./hooks/useFlyers', () => ({
  useFlyers: () => mockUseFlyers(),
}));
const mockUseMasterItems = vi.fn();
vi.mock('./hooks/useMasterItems', () => ({
  useMasterItems: () => mockUseMasterItems(),
}));
const mockUseUserData = vi.fn();
vi.mock('./hooks/useUserData', () => ({ useUserData: () => mockUseUserData() }));

// Mock top-level components rendered by App's routes
vi.mock('./components/Header', () => ({ Header: (props: HeaderProps) => <header data-testid="header-mock"><button onClick={props.onOpenProfile}>Open Profile</button><button onClick={props.onOpenVoiceAssistant}>Open Voice Assistant</button></header> }));
vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: ({ isOpen, onClose, onProfileUpdate, onLoginSuccess }: { isOpen: boolean, onClose: () => void, onProfileUpdate: (p: Profile) => void, onLoginSuccess: (u: User, t: string, r: boolean) => void }) => isOpen ? <div data-testid="profile-manager-mock"><button onClick={onClose}>Close Profile</button><button onClick={() => onProfileUpdate({ user_id: '1', role: 'user', points: 0, full_name: 'Updated' })}>Update Profile</button><button onClick={() => onLoginSuccess({ user_id: '1', email: 'a@b.com' }, 'token', false)}>Login</button></div> : null }));
vi.mock('./features/voice-assistant/VoiceAssistant', () => ({ VoiceAssistant: ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => isOpen ? <div data-testid="voice-assistant-mock"><button onClick={onClose}>Close Voice Assistant</button></div> : null }));
vi.mock('./pages/admin/AdminPage', () => ({ AdminPage: () => <div data-testid="admin-page-mock">Admin Page</div> }));
// In react-router v6, wrapper routes must render an <Outlet /> for nested routes to appear.
vi.mock('./components/AdminRoute', () => ({ AdminRoute: ({ children }: { children: React.ReactNode }) => <div data-testid="admin-route-mock">{children || <Outlet />}</div> }));
vi.mock('./pages/admin/CorrectionsPage', () => ({ CorrectionsPage: () => <div data-testid="corrections-page-mock">Corrections Page</div> }));
vi.mock('./pages/admin/AdminStatsPage', () => ({ AdminStatsPage: () => <div data-testid="admin-stats-page-mock">Admin Stats Page</div> }));
vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () => <div data-testid="reset-password-page-mock">Reset Password Page</div> }));
vi.mock('./pages/admin/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> })); // Corrected path
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => isOpen ? <div data-testid="whats-new-modal-mock"><button onClick={onClose}>Close What's New</button></div> : null }));
vi.mock('./components/FlyerCorrectionTool', () => ({ FlyerCorrectionTool: ({ isOpen, onClose, onDataExtracted }: { isOpen: boolean, onClose: () => void, onDataExtracted: (type: 'store_name' | 'dates', value: string) => void }) => isOpen ? <div data-testid="flyer-correction-tool-mock"><button onClick={onClose}>Close Correction</button><button onClick={() => onDataExtracted('store_name', 'New Store Name')}>Extract Store</button><button onClick={() => onDataExtracted('dates', '2023-01-01')}>Extract Dates</button></div> : null }));
// Mock the new layout and page components
vi.mock('./layouts/MainLayout', () => ({ MainLayout: () => <div data-testid="main-layout-mock"><Outlet /></div> }));
vi.mock('./pages/HomePage', () => ({ HomePage: (props: { onOpenCorrectionTool: () => void }) => <div data-testid="home-page-mock"><button onClick={props.onOpenCorrectionTool}>Open Correction Tool</button></div> }));

// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
// This must be done in any test file that imports App.tsx.
vi.mock('pdfjs-dist', () => ({
  // pdfjsLib: { GlobalWorkerOptions: { workerSrc: '' } },
  GlobalWorkerOptions: { workerSrc: '' },
  getDocument: vi.fn(() => ({
    promise: Promise.resolve({ getPage: vi.fn() }),
  })),
}));

// Mock the new config module
vi.mock('./config', () => ({
  default: {
    app: { version: '1.0.0', commitMessage: 'Initial commit', commitUrl: '#' },
    google: { mapsEmbedApiKey: 'mock-key' },
  },
}));

const mockedAiApiClient = vi.mocked(aiApiClient); // Mock aiApiClient
const mockedApiClient = vi.mocked(apiClient);

// Mock the useShoppingLists hook
vi.mock('./hooks/useShoppingLists', () => ({ useShoppingLists: vi.fn() }));
// Mock the useWatchedItems hook
vi.mock('./hooks/useWatchedItems', () => ({ useWatchedItems: vi.fn() }));

const mockFlyers: Flyer[] = [
    { flyer_id: 1, file_name: 'flyer1.jpg', image_url: 'url1', item_count: 10, created_at: '2023-01-01', store: { store_id: 1, name: 'Old Store', created_at: '2023-01-01' } },
    { flyer_id: 2, file_name: 'flyer2.jpg', image_url: 'url2', item_count: 20, created_at: '2023-01-02', store: { store_id: 2, name: 'Another Store', created_at: '2023-01-01' } },
];

describe('App Component', () => {
    // Mock localStorage
    let storage: { [key: string]: string } = {};
    const localStorageMock = {
        getItem: vi.fn((key: string) => storage[key] || null),
        setItem: vi.fn((key: string, value: string) => { storage[key] = value; }),
        removeItem: vi.fn((key: string) => { delete storage[key]; }),
        clear: vi.fn(() => { storage = {}; }),
    };

    // Mock matchMedia
    const matchMediaMock = vi.fn().mockImplementation(query => ({
        matches: false, // Default to light mode
        media: query,
        onchange: null,
        addListener: vi.fn(), // deprecated
        removeListener: vi.fn(), // deprecated
        addEventListener: vi.fn(),
        removeEventListener: vi.fn(),
        dispatchEvent: vi.fn(),
    }));

    beforeEach(() => {
        vi.clearAllMocks();
        // Default auth state: loading or guest
        // Mock the login function to simulate a successful login.
        const mockLogin = vi.fn(async (_user: User, _token: string) => {
            await act(async () => {
                // Simulate fetching profile after login
                const profileResponse = await mockedApiClient.getAuthenticatedUserProfile();
                const userProfile: UserProfile = await profileResponse.json();
                mockUseAuth.mockReturnValue({
                    user: userProfile.user,
                    profile: userProfile,
                    authStatus: 'AUTHENTICATED',
                    isLoading: false,
                    login: mockLogin, // Self-reference the mock
                    logout: vi.fn(),
                    updateProfile: vi.fn(),
                });
            });
        });
        mockUseAuth.mockReturnValue({
            user: null,
            profile: null,
            authStatus: 'SIGNED_OUT',
            isLoading: false, // Start with isLoading: false for most tests
            login: mockLogin,
            logout: vi.fn(),
            updateProfile: vi.fn(),
        });

        // Default data states for the new hooks
        mockUseFlyers.mockReturnValue({
            flyers: mockFlyers,
            isLoadingFlyers: false,
        });
        mockUseMasterItems.mockReturnValue({
            masterItems: [],
            isLoading: false,
        });
        mockUseUserData.mockReturnValue({
            watchedItems: [],
            shoppingLists: [],
            setWatchedItems: vi.fn(),
            setShoppingLists: vi.fn(),
        });
        // Clear local storage to prevent state from leaking between tests.
        localStorage.clear();
        Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true });
        Object.defineProperty(window, 'matchMedia', { value: matchMediaMock, configurable: true });

        // Default mocks for API calls
        // Use mockImplementation to create a new Response object for each call,
        // preventing "Body has already been read" errors.
        // Use mockImplementation to create a new Response object for each call,
        // preventing "Body has already been read" errors.
        mockedApiClient.fetchFlyers.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([]))));
        // Mock getAuthenticatedUserProfile as it's called by useAuth's checkAuthToken and login
        mockedApiClient.getAuthenticatedUserProfile.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({
            user_id: 'test-user-id',
            user: { user_id: 'test-user-id', email: 'test@example.com' },
            full_name: 'Test User',
            avatar_url: '',
            role: 'user',
            points: 0,
        }))));
        mockedApiClient.fetchMasterItems.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([]))));
        mockedApiClient.fetchWatchedItems.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([]))));
        mockedApiClient.fetchShoppingLists.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([]))));
        mockedAiApiClient.rescanImageArea.mockResolvedValue(new Response(JSON.stringify({ text: 'mocked text' }))); // Mock for FlyerCorrectionTool
    });

    const renderApp = (initialEntries = ['/']) => {
        return render(
            <MemoryRouter initialEntries={initialEntries}>
                <AppProviders>
                    <App />
                </AppProviders>
            </MemoryRouter>
        );
    };

    it('should render the main layout and header', async () => {
        // Simulate the auth hook finishing its initial check
        mockUseAuth.mockReturnValue({
            user: null,
            profile: null,
            authStatus: 'SIGNED_OUT',
            isLoading: false,
            login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
        });

        renderApp();
        await waitFor(() => {
            expect(screen.getByTestId('header-mock')).toBeInTheDocument();
            // Check that the main layout and home page are rendered for the root path
            expect(screen.getByTestId('main-layout-mock')).toBeInTheDocument();
            expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
        });
    });

    it('should render the BulkImporter for an admin user', async () => {
        const mockAdminProfile: UserProfile = {
            user_id: 'admin-id',
            user: { user_id: 'admin-id', email: 'admin@example.com' },
            role: 'admin',
            full_name: 'Admin',
            avatar_url: '',
            points: 0,
        };
        
        // Force the auth hook to return an authenticated admin user
        mockUseAuth.mockReturnValue({
            user: mockAdminProfile.user,
            profile: mockAdminProfile,
            authStatus: 'AUTHENTICATED', 
            isLoading: false,
            login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
        });

        renderApp(['/admin']);
        
        await waitFor(() => {
            expect(screen.getByTestId('header-mock')).toBeInTheDocument();
            expect(screen.getByTestId('admin-page-mock')).toBeInTheDocument();
        });
    });

    it('should show a welcome message when no flyer is selected', async () => {
        // Simulate the auth hook finishing its initial check
        mockUseAuth.mockReturnValue({
            user: null,
            profile: null,
            authStatus: 'SIGNED_OUT',
            isLoading: false,
            login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
        });

        renderApp();
        await waitFor(() => {
            // The welcome message is inside HomePage, which is now mocked. We check for the mock instead.
            expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
        });
    });

    it('should render the admin page on the /admin route', async () => {
        // Mock a logged-in admin user
        const mockAdminProfile: UserProfile = {
            user_id: 'admin-id',
            user: { user_id: 'admin-id', email: 'admin@example.com' },
            role: 'admin',
            full_name: 'Admin',
            avatar_url: '',
            points: 0,
        };

        // Directly set the auth state via the mocked hook
        mockUseAuth.mockReturnValue({
            user: mockAdminProfile.user,
            profile: mockAdminProfile,
            authStatus: 'AUTHENTICATED',
            isLoading: false,
            login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
        });

        renderApp(['/admin']);

        expect(await screen.findByTestId('admin-page-mock')).toBeInTheDocument();
    });

    it('should render the reset password page on the correct route', async () => {
        renderApp(['/reset-password/some-token']);

        await waitFor(() => {
            expect(screen.getByTestId('reset-password-page-mock')).toBeInTheDocument();
        });
    });

    describe('Theme and Unit System Synchronization', () => {
        it('should set dark mode based on user profile preferences', () => {
            const profileWithDarkMode: UserProfile = {
                user_id: 'user-1', user: { user_id: 'user-1', email: 'dark@mode.com' }, role: 'user', points: 0,
                preferences: { darkMode: true }
            };
            mockUseAuth.mockReturnValue({
                user: profileWithDarkMode.user, profile: profileWithDarkMode, authStatus: 'AUTHENTICATED',
                isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
            });

            renderApp();
            expect(document.documentElement).toHaveClass('dark');
        });

        it('should set dark mode based on localStorage if profile has no preference', () => {
            localStorageMock.setItem('darkMode', 'true');
            renderApp();
            expect(document.documentElement).toHaveClass('dark');
        });

        it('should set dark mode based on system preference if no other setting exists', () => {
            matchMediaMock.mockImplementationOnce(query => ({ matches: true, media: query }));
            renderApp();
            expect(document.documentElement).toHaveClass('dark');
        });

        it('should set unit system based on user profile preferences', async () => {
            const profileWithMetric: UserProfile = {
                user_id: 'user-1', user: { user_id: 'user-1', email: 'metric@user.com' }, role: 'user', points: 0,
                preferences: { unitSystem: 'metric' }
            };
            mockUseAuth.mockReturnValue({
                user: profileWithMetric.user, profile: profileWithMetric, authStatus: 'AUTHENTICATED',
                isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
            });

            renderApp();
            // The unit system is passed as a prop to Header, which is mocked.
            // We can't directly see the result in the DOM easily, so we trust the state is set.
            // A more integrated test would be needed to verify the Header receives the prop.
            // For now, this test ensures the useEffect logic runs without crashing.
            await waitFor(() => {
                expect(screen.getByTestId('header-mock')).toBeInTheDocument();
            });
        });
    });

    describe('OAuth Token Handling', () => {
        it('should call login when a googleAuthToken is in the URL', async () => {
            const mockLogin = vi.fn().mockResolvedValue(undefined);
            mockUseAuth.mockReturnValue({
                user: null, profile: null, authStatus: 'SIGNED_OUT', isLoading: false,
                login: mockLogin, logout: vi.fn(), updateProfile: vi.fn(),
            });

            renderApp(['/?googleAuthToken=test-google-token']);

            await waitFor(() => {
                expect(mockLogin).toHaveBeenCalledWith({ user_id: '', email: '' }, 'test-google-token');
            });
        });

        it('should call login when a githubAuthToken is in the URL', async () => {
            const mockLogin = vi.fn().mockResolvedValue(undefined);
            mockUseAuth.mockReturnValue({
                user: null, profile: null, authStatus: 'SIGNED_OUT', isLoading: false,
                login: mockLogin, logout: vi.fn(), updateProfile: vi.fn(),
            });

            renderApp(['/?githubAuthToken=test-github-token']);

            await waitFor(() => {
                expect(mockLogin).toHaveBeenCalledWith({ user_id: '', email: '' }, 'test-github-token');
            });
        });

        it('should log an error if login with a token fails', async () => {
            const mockLogin = vi.fn().mockRejectedValue(new Error('Token login failed'));
            mockUseAuth.mockReturnValue({
                user: null, profile: null, authStatus: 'SIGNED_OUT', isLoading: false,
                login: mockLogin, logout: vi.fn(), updateProfile: vi.fn(),
            });

            renderApp(['/?googleAuthToken=bad-token']);
            await waitFor(() => expect(mockLogin).toHaveBeenCalled());
        });
    });

    describe('Flyer Selection from URL', () => {
        it('should select a flyer when flyerId is present in the URL', async () => {
            renderApp(['/flyers/2']);

            // The HomePage mock will be rendered. The important part is that the selection logic
            // in App.tsx runs and passes the correct `selectedFlyer` prop down.
            // Since HomePage is mocked, we can't see the direct result, but we can
            // infer that the logic ran without crashing and the correct route was matched.
            await waitFor(() => {
                expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
            });
        });

        it('should select the first flyer if no flyer is selected and flyers are available', async () => {
            renderApp(['/']);

            await waitFor(() => {
                expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
            });
        });
    });

    describe('Version and "What\'s New" Modal', () => {
        it('should show the "What\'s New" modal if the app version is new', async () => {
            // Mock the config module for this specific test
            vi.mock('./config', () => ({
              default: {
                app: { version: '1.0.1', commitMessage: 'New feature!', commitUrl: '#' },
                google: { mapsEmbedApiKey: 'mock-key' },
              },
            }));
            localStorageMock.setItem('lastSeenVersion', '1.0.0');
            renderApp();
            await expect(screen.findByTestId('whats-new-modal-mock')).resolves.toBeInTheDocument();
        });
    });

    describe('Modal Interactions', () => {
        it('should open and close the ProfileManager modal', async () => {
            renderApp();
            expect(screen.queryByTestId('profile-manager-mock')).not.toBeInTheDocument();

            // Open modal
            fireEvent.click(screen.getByText('Open Profile'));
            expect(await screen.findByTestId('profile-manager-mock')).toBeInTheDocument();

            // Close modal
            fireEvent.click(screen.getByText('Close Profile'));
            await waitFor(() => {
                expect(screen.queryByTestId('profile-manager-mock')).not.toBeInTheDocument();
            });
        });

        it('should open and close the VoiceAssistant modal for authenticated users', async () => {
            mockUseAuth.mockReturnValue({
                user: { user_id: '1', email: 'test@test.com' },
                profile: { user_id: '1', role: 'user', points: 0, user: { user_id: '1', email: 'test@test.com' } },
                authStatus: 'AUTHENTICATED',
                isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
            });
            renderApp();
            expect(screen.queryByTestId('voice-assistant-mock')).not.toBeInTheDocument();

            // Open modal
            fireEvent.click(screen.getByText('Open Voice Assistant'));
            expect(await screen.findByTestId('voice-assistant-mock')).toBeInTheDocument();

            // Close modal
            fireEvent.click(screen.getByText('Close Voice Assistant'));
            await waitFor(() => {
                expect(screen.queryByTestId('voice-assistant-mock')).not.toBeInTheDocument();
            });
        });

        it('should open and close the FlyerCorrectionTool modal', async () => {
            renderApp();
            expect(screen.queryByTestId('flyer-correction-tool-mock')).not.toBeInTheDocument();

            // Open modal
            fireEvent.click(screen.getByText('Open Correction Tool'));
            expect(await screen.findByTestId('flyer-correction-tool-mock')).toBeInTheDocument();

            // Close modal
            fireEvent.click(screen.getByText('Close Correction'));
            await waitFor(() => {
                expect(screen.queryByTestId('flyer-correction-tool-mock')).not.toBeInTheDocument();
            });
        });
    });

    describe('Flyer Correction Tool Data Handling', () => {
        it('should handle store name extraction from the correction tool', async () => {
            renderApp();
            fireEvent.click(screen.getByText('Open Correction Tool'));
            const correctionTool = await screen.findByTestId('flyer-correction-tool-mock');
            // We trigger the callback from the mock and ensure it doesn't crash.
            fireEvent.click(within(correctionTool).getByText('Extract Store'));
            // The test passes if no errors are thrown here.
        });

        it('should handle date extraction from the correction tool', async () => {
            renderApp();
            fireEvent.click(screen.getByText('Open Correction Tool'));
            const correctionTool = await screen.findByTestId('flyer-correction-tool-mock');
            fireEvent.click(within(correctionTool).getByText('Extract Dates'));
            // The test passes if no errors are thrown here, covering the 'dates' branch.
        });
    });

    describe('Version Display and What\'s New', () => {
        beforeEach(() => {
            // Also mock the config module to reflect this change
            vi.mock('./config', () => ({
              default: {
                app: { version: '2.0.0', commitMessage: 'A new version!', commitUrl: 'http://example.com/commit/2.0.0' },
                google: { mapsEmbedApiKey: 'mock-key' },
              },
            }));
        });

        it('should display the version number and commit link', () => {
            renderApp();
            const versionLink = screen.getByText(`Version: 2.0.0`);
            expect(versionLink).toBeInTheDocument();
            expect(versionLink).toHaveAttribute('href', 'http://example.com/commit/2.0.0');
        });

        it('should open the "What\'s New" modal when the question mark icon is clicked', async () => {
            // Pre-set the localStorage to prevent the modal from opening automatically
            localStorageMock.setItem('lastSeenVersion', '2.0.0');

            renderApp();
            expect(screen.queryByTestId('whats-new-modal-mock')).not.toBeInTheDocument();

            const openButton = await screen.findByTitle("Show what's new in this version");
            fireEvent.click(openButton);

            expect(await screen.findByTestId('whats-new-modal-mock')).toBeInTheDocument();
        });
    });

    describe('Dynamic Toaster Styles', () => {
        it('should render the correct CSS variables for toast styling in light mode', () => {
            renderApp();
            const styleTag = document.querySelector('style');
            expect(styleTag).not.toBeNull();
            expect(styleTag!.innerHTML).toContain('--toast-bg: #FFFFFF');
            expect(styleTag!.innerHTML).toContain('--toast-color: #1F2937');
        });

        it('should render the correct CSS variables for toast styling in dark mode', () => {
            localStorageMock.setItem('darkMode', 'true');
            renderApp();
            const styleTag = document.querySelector('style');
            expect(styleTag).not.toBeNull();
            expect(styleTag!.innerHTML).toContain('--toast-bg: #4B5563');
        });
    });

    describe('Profile and Login Handlers', () => {
        it('should call updateProfile when handleProfileUpdate is triggered', async () => {
            const mockUpdateProfile = vi.fn();
            mockUseAuth.mockReturnValue({
                ...mockUseAuth(),
                updateProfile: mockUpdateProfile,
            });

            renderApp();
            fireEvent.click(screen.getByText('Open Profile'));
            const profileManager = await screen.findByTestId('profile-manager-mock');
            fireEvent.click(within(profileManager).getByText('Update Profile'));

            expect(mockUpdateProfile).toHaveBeenCalledWith({ user_id: '1', role: 'user', points: 0, full_name: 'Updated' });
        });

        it('should set an error state if login fails inside handleLoginSuccess', async () => {
            const mockLogin = vi.fn().mockRejectedValue(new Error('Login failed'));
            mockUseAuth.mockReturnValue({ ...mockUseAuth(), login: mockLogin, user: null, profile: null, authStatus: 'SIGNED_OUT', isLoading: false, logout: vi.fn(), updateProfile: vi.fn() });

            renderApp();
            fireEvent.click(screen.getByText('Open Profile'));
            const loginButton = await screen.findByText('Login');
            fireEvent.click(loginButton);

            // We need to wait for the async login function to be called and reject.
            await waitFor(() => {
                expect(mockLogin).toHaveBeenCalled();
            });
        });
    });
});