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

// Mock the entire apiClient module to ensure all exports are defined.
// This is the primary fix for the error: [vitest] No "..." export is defined on the mock.
vi.mock('../../../services/apiClient', () => ({
  pingBackend: vi.fn(),
  checkStorage: vi.fn(),
  checkDbPoolHealth: vi.fn(),
  checkPm2Status: vi.fn(),
  checkRedisHealth: vi.fn(),
  checkDbSchema: vi.fn(),
  loginUser: vi.fn(),
  triggerFailingJob: vi.fn(),
  clearGeocodeCache: vi.fn(),
}));

// Get a type-safe mocked version of the apiClient module.
const mockedApiClient = vi.mocked(apiClient);

// Correct the relative path to the logger module.
vi.mock('../../../services/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

// Mock toast to check for notifications
vi.mock('react-hot-toast', () => ({
  __esModule: true,
  default: {
    success: vi.fn(),
    error: vi.fn(),
  },
}));

describe('SystemCheck', () => {
  // Store original env variable
  const originalGeminiApiKey = import.meta.env.GEMINI_API_KEY;

  beforeEach(() => {
    vi.clearAllMocks();
    // Use `mockImplementation` to create a new Response object for every call.
    // This prevents "Body has already been read" errors and memory leaks.
    mockedApiClient.pingBackend.mockImplementation(() => Promise.resolve(new Response('pong')));
    mockedApiClient.checkStorage.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ success: true, message: 'Storage OK' }))),
    );
    mockedApiClient.checkDbPoolHealth.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ success: true, message: 'DB Pool OK' }))),
    );
    mockedApiClient.checkPm2Status.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ success: true, message: 'PM2 OK' }))),
    );
    mockedApiClient.checkRedisHealth.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ success: true, message: 'Redis OK' }))),
    );
    mockedApiClient.checkDbSchema.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ success: true, message: 'Schema OK' }))),
    );
    mockedApiClient.loginUser.mockImplementation(() =>
      Promise.resolve(
        new Response(JSON.stringify({ user: createMockUser(), token: '' }), { status: 200 }),
      ),
    );
    mockedApiClient.triggerFailingJob.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ message: 'Job triggered!' }))),
    );
    mockedApiClient.clearGeocodeCache.mockImplementation(() =>
      Promise.resolve(new Response(JSON.stringify({ message: 'Cache cleared!' }))),
    );

    // Reset GEMINI_API_KEY for each test to its original value.
    setGeminiApiKey(originalGeminiApiKey);
  });

  // Restore all mocks after each test to ensure test isolation.
  afterEach(() => {
    vi.restoreAllMocks();
    cleanup();
  });

  // Helper to set GEMINI_API_KEY for specific tests.
  const setGeminiApiKey = (value: string | undefined) => {
    if (value === undefined) {
      delete (import.meta.env as Record<string, string | undefined>).GEMINI_API_KEY;
    } else {
      (import.meta.env as Record<string, string | undefined>).GEMINI_API_KEY = value;
    }
  };

  it('should render initial idle state and then run checks automatically on mount', async () => {
    setGeminiApiKey('mock-api-key');
    render(<SystemCheck />);

    // Initially, all checks should be in 'running' state due to auto-run
    // However, the API key check is synchronous and resolves immediately.
    // All 8 checks now run asynchronously.
    expect(screen.getAllByText('Checking...')).toHaveLength(8);

    // Wait for all checks to complete
    await waitFor(() => {
      expect(screen.getByText('GEMINI_API_KEY is set.')).toBeInTheDocument();
      expect(screen.getByText('Backend server is running and reachable.')).toBeInTheDocument();
      expect(screen.getByText('Redis OK')).toBeInTheDocument();
      expect(screen.getByText('PM2 OK')).toBeInTheDocument();
      expect(screen.getByText('DB Pool OK')).toBeInTheDocument();
      expect(screen.getByText('Schema OK')).toBeInTheDocument();
      expect(screen.getByText('Default admin user login was successful.')).toBeInTheDocument();
      expect(screen.getByText('Storage OK')).toBeInTheDocument();
    });

    // Check that the re-run button is enabled and elapsed time is shown
    expect(screen.getByRole('button', { name: /re-run checks/i })).toBeEnabled();
    expect(screen.getByText(/finished in \d+\.\d{2} seconds\./i)).toBeInTheDocument();
  });

  it('should show API key as failed if GEMINI_API_KEY is not set', async () => {
    setGeminiApiKey(undefined);
    render(<SystemCheck />);

    // Wait for the specific error message to appear.
    expect(
      await screen.findByText('GEMINI_API_KEY is missing. AI features will not work.'),
    ).toBeInTheDocument();
    // Crucially, other checks should still pass.
    expect(await screen.findByText('Backend server is running and reachable.')).toBeInTheDocument();
  });

  it('should show backend connection as failed if pingBackend fails', async () => {
    setGeminiApiKey('mock-api-key');
    (mockedApiClient.pingBackend as Mock).mockRejectedValueOnce(new Error('Network error'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('Network error')).toBeInTheDocument();
      // Multiple checks will be skipped, so we use getAllByText.
      const skippedMessages = screen.getAllByText('Skipped: Backend server is not reachable.'); // Redis is now also skipped
      expect(skippedMessages.length).toBe(6); // PM2, DB Pool, Redis, Schema, Seed, Storage
    });

    // Dependent checks should be skipped/failed
    expect(mockedApiClient.checkDbPoolHealth).not.toHaveBeenCalled();
    expect(mockedApiClient.checkDbSchema).not.toHaveBeenCalled();
    expect(mockedApiClient.loginUser).not.toHaveBeenCalled();
    expect(mockedApiClient.checkStorage).not.toHaveBeenCalled();
    expect(mockedApiClient.checkPm2Status).not.toHaveBeenCalled();
    expect(mockedApiClient.checkRedisHealth).not.toHaveBeenCalled();
  });

  it('should show PM2 status as failed if checkPm2Status returns success: false', async () => {
    setGeminiApiKey('mock-api-key'); // This was missing
    mockedApiClient.checkPm2Status.mockImplementationOnce(() =>
      Promise.resolve(
        new Response(JSON.stringify({ success: false, message: 'PM2 process not found' })),
      ),
    );
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('PM2 process not found')).toBeInTheDocument();
    });
  });

  it('should show database pool check as failed if checkDbPoolHealth fails', async () => {
    setGeminiApiKey('mock-api-key'); // This was missing
    mockedApiClient.checkDbPoolHealth.mockRejectedValueOnce(new Error('DB connection refused'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('DB connection refused')).toBeInTheDocument();
    });
  });

  it('should show Redis check as failed if checkRedisHealth fails', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.checkRedisHealth.mockRejectedValueOnce(new Error('Redis connection refused'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('Redis connection refused')).toBeInTheDocument();
    });
  });

  it('should skip schema and seed checks if DB pool check fails', async () => {
    setGeminiApiKey('mock-api-key');
    // Mock the DB pool check to fail
    mockedApiClient.checkDbPoolHealth.mockImplementationOnce(() =>
      Promise.reject(new Error('DB connection refused')),
    );
    render(<SystemCheck />);

    await waitFor(() => {
      // Verify the specific "skipped" messages for DB-dependent checks
      expect(screen.getAllByText('Skipped: Database connection pool is unhealthy.')).toHaveLength(
        2,
      );
      // Verify other parallel checks still run and pass
      expect(screen.getByText('Storage OK')).toBeInTheDocument();
    });
  });

  it('should show database schema check as failed if checkDbSchema fails', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.checkDbSchema.mockImplementationOnce(() =>
      Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))),
    );
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('Schema mismatch')).toBeInTheDocument();
    });
  });

  it('should show seeded user check as failed if loginUser fails', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Incorrect email or password'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(
        screen.getByText('Login failed. Ensure the default admin user is seeded in your database.'),
      ).toBeInTheDocument();
    });
  });

  it('should show a generic failure message for other login errors', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Server is on fire'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('Failed: Server is on fire')).toBeInTheDocument();
    });
  });

  it('should show storage directory check as failed if checkStorage fails', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.checkStorage.mockRejectedValueOnce(new Error('Storage not writable'));
    render(<SystemCheck />);

    await waitFor(() => {
      expect(screen.getByText('Storage not writable')).toBeInTheDocument();
    });
  });

  it('should display a loading spinner and disable button while checks are running', async () => {
    setGeminiApiKey('mock-api-key');
    // Create a promise we can resolve manually to control the loading state
    let resolvePromise: (value: Response) => void;
    const mockPromise = new Promise<Response>((resolve) => {
      resolvePromise = resolve;
    });
    mockedApiClient.pingBackend.mockImplementation(() => mockPromise);

    render(<SystemCheck />);

    // The button text changes to "Running Checks..."
    const runningButton = screen.getByRole('button', { name: /running checks/i });
    expect(runningButton).toBeDisabled();
    expect(runningButton.querySelector('svg')).toBeInTheDocument(); // Check for spinner

    // Now resolve the promise to allow the test to clean up properly
    await act(async () => {
      resolvePromise(new Response('pong'));
      await mockPromise;
    });

    // Wait for the button to become enabled again
    await waitFor(() => {
      expect(screen.getByRole('button', { name: /re-run checks/i })).toBeEnabled();
    });
  });

  it('should re-run checks when the "Re-run Checks" button is clicked', async () => {
    setGeminiApiKey('mock-api-key');
    render(<SystemCheck />);

    // Wait for initial auto-run to complete
    await waitFor(() => expect(screen.getByText(/finished in/i)).toBeInTheDocument());

    // Reset mocks for the re-run with new messages to ensure we are testing the re-run
    mockedApiClient.checkDbSchema.mockImplementationOnce(() =>
      Promise.resolve(
        new Response(JSON.stringify({ success: true, message: 'Schema OK (re-run)' })),
      ),
    );
    mockedApiClient.checkStorage.mockImplementationOnce(() =>
      Promise.resolve(
        new Response(JSON.stringify({ success: true, message: 'Storage OK (re-run)' })),
      ),
    );
    mockedApiClient.checkDbPoolHealth.mockImplementationOnce(() =>
      Promise.resolve(
        new Response(JSON.stringify({ success: true, message: 'DB Pool OK (re-run)' })),
      ),
    );

    const rerunButton = screen.getByRole('button', { name: /re-run checks/i });
    fireEvent.click(rerunButton);

    // Expect checks to go back to 'Checking...' state
    await waitFor(() => {
      expect(screen.getAllByText('Checking...')).toHaveLength(8);
    });

    // Wait for re-run to complete and check for the new messages
    await waitFor(() => {
      expect(screen.getByText('Schema OK (re-run)')).toBeInTheDocument();
      expect(screen.getByText('Storage OK (re-run)')).toBeInTheDocument();
      expect(screen.getByText('DB Pool OK (re-run)')).toBeInTheDocument();
      expect(screen.getByText('PM2 OK')).toBeInTheDocument(); // This one doesn't get a new message in this mock setup
    });
    expect(mockedApiClient.pingBackend).toHaveBeenCalledTimes(2); // Initial run + re-run
  });

  it('should display correct icons for each status (pass and fail)', async () => {
    setGeminiApiKey('mock-api-key');
    mockedApiClient.checkDbSchema.mockImplementationOnce(() =>
      Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))),
    );
    const { container } = render(<SystemCheck />);

    await waitFor(() => {
      // Instead of test-ids, we check for the result: the icon's color class.
      // This is more robust as it doesn't depend on the icon component's internal props.
      const passIcons = container.querySelectorAll('.text-green-500');
      expect(passIcons.length).toBeGreaterThan(0);

      // Check for the fail icon's color class
      const failIcon = container.querySelector('svg.text-red-500');
      expect(failIcon).toBeInTheDocument();
    });
  });

  it('should display elapsed time after checks complete', async () => {
    setGeminiApiKey('mock-api-key');
    render(<SystemCheck />);

    await waitFor(() => {
      const elapsedTimeText = screen.getByText(/finished in \d+\.\d{2} seconds\./i);
      expect(elapsedTimeText).toBeInTheDocument();
      const match = elapsedTimeText.textContent?.match(/(\d+\.\d{2})/);
      expect(match).not.toBeNull();
      expect(parseFloat(match![1])).toBeGreaterThan(0);
    });
  });

  describe('Integration: Job Queue Retries', () => {
    it('should call triggerFailingJob and show a success toast', async () => {
      render(<SystemCheck />);
      const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
      fireEvent.click(triggerButton);

      await waitFor(() => {
        expect(mockedApiClient.triggerFailingJob).toHaveBeenCalled();
        expect(vi.mocked(toast).success).toHaveBeenCalledWith('Job triggered!');
      });
    });

    it('should show a loading state while triggering the job', async () => {
      let resolvePromise: (value: Response) => void;
      const mockPromise = new Promise<Response>((resolve) => {
        resolvePromise = resolve;
      });
      mockedApiClient.triggerFailingJob.mockImplementation(() => mockPromise);

      render(<SystemCheck />);
      const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
      fireEvent.click(triggerButton);

      await waitFor(() => {
        expect(screen.getByRole('button', { name: /triggering/i })).toBeDisabled();
      });

      await act(async () => {
        resolvePromise(new Response(JSON.stringify({ message: 'Job triggered!' })));
        await mockPromise;
      });
    });

    it('should show an error toast if triggering the job fails', async () => {
      mockedApiClient.triggerFailingJob.mockRejectedValueOnce(new Error('Queue is down'));
      render(<SystemCheck />);
      const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
      fireEvent.click(triggerButton);

      await waitFor(() => {
        expect(vi.mocked(toast).error).toHaveBeenCalledWith('Queue is down');
      });
    });

    it('should show an error toast if the API returns a non-OK response', async () => {
      mockedApiClient.triggerFailingJob.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Server error' }), { status: 500 }),
      );
      render(<SystemCheck />);
      const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
      fireEvent.click(triggerButton);

      await waitFor(() => {
        expect(vi.mocked(toast).error).toHaveBeenCalledWith('Server error');
      });
    });
  });

  describe('GeocodeCacheManager', () => {
    beforeEach(() => {
      // Mock window.confirm to always return true for these tests
      vi.spyOn(window, 'confirm').mockReturnValue(true);
    });

    it('should call clearGeocodeCache and show a success toast', async () => {
      render(<SystemCheck />);
      // Wait for checks to run and Redis to be OK
      await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());

      const clearButton = screen.getByRole('button', { name: /clear geocode cache/i });
      fireEvent.click(clearButton);

      await waitFor(() => {
        expect(mockedApiClient.clearGeocodeCache).toHaveBeenCalled();
        expect(vi.mocked(toast).success).toHaveBeenCalledWith('Cache cleared!');
      });
    });

    it('should show an error toast if clearing the cache fails', async () => {
      mockedApiClient.clearGeocodeCache.mockRejectedValueOnce(new Error('Redis is busy'));
      render(<SystemCheck />);
      await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());
      fireEvent.click(screen.getByRole('button', { name: /clear geocode cache/i }));
      await waitFor(() => expect(vi.mocked(toast).error).toHaveBeenCalledWith('Redis is busy'));
    });

    it('should not call clearGeocodeCache if user cancels confirmation', async () => {
      vi.spyOn(window, 'confirm').mockReturnValue(false);
      render(<SystemCheck />);
      await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());

      const clearButton = screen.getByRole('button', { name: /clear geocode cache/i });
      fireEvent.click(clearButton);

      expect(mockedApiClient.clearGeocodeCache).not.toHaveBeenCalled();
    });

    it('should show an error toast if the API returns a non-OK response', async () => {
      mockedApiClient.clearGeocodeCache.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Cache clear failed' }), { status: 500 }),
      );
      render(<SystemCheck />);
      await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());

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

      await waitFor(() => {
        expect(vi.mocked(toast).error).toHaveBeenCalledWith('Cache clear failed');
      });
    });

    it('should hide Redis controls if Redis check fails', async () => {
      mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
        new Response(JSON.stringify({ success: false, message: 'Redis down' })),
      );
      render(<SystemCheck />);

      await waitFor(() => expect(screen.getByText('Redis down')).toBeInTheDocument());

      expect(screen.queryByTitle('Redis cache is connected')).not.toBeInTheDocument();
      expect(
        screen.queryByRole('button', { name: /clear geocode cache/i }),
      ).not.toBeInTheDocument();
    });
  });

  describe('Additional Edge Cases', () => {
    it('should fail backend check if response text is not "pong"', async () => {
      mockedApiClient.pingBackend.mockResolvedValueOnce(
        new Response('unexpected response', { status: 200 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(
          screen.getByText('Backend server is not responding. Is it running?'),
        ).toBeInTheDocument();
      });
    });

    it('should handle non-OK response for generic checks (e.g. Storage)', async () => {
      mockedApiClient.checkStorage.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Permission denied' }), { status: 403 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('Permission denied')).toBeInTheDocument();
      });
    });

    it('should handle non-OK response from checkDbSchema', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.checkDbSchema.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Schema check failed 500' }), { status: 500 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('Schema check failed 500')).toBeInTheDocument();
      });
    });

    it('should handle non-OK response from checkDbPoolHealth', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.checkDbPoolHealth.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'DB Pool check failed 500' }), { status: 500 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('DB Pool check failed 500')).toBeInTheDocument();
      });
    });

    it('should handle non-OK response from checkPm2Status', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.checkPm2Status.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'PM2 check failed 500' }), { status: 500 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('PM2 check failed 500')).toBeInTheDocument();
      });
    });

    it('should handle non-OK response from checkRedisHealth', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Redis check failed 500' }), { status: 500 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('Redis check failed 500')).toBeInTheDocument();
      });
    });

    it('should handle Redis check returning success: false', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
        new Response(JSON.stringify({ success: false, message: 'Redis is down' })),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('Redis is down')).toBeInTheDocument();
      });
    });

    it('should handle non-OK response from loginUser', async () => {
      setGeminiApiKey('mock-api-key');
      mockedApiClient.loginUser.mockResolvedValueOnce(
        new Response(JSON.stringify({ message: 'Invalid credentials' }), { status: 401 }),
      );
      render(<SystemCheck />);

      await waitFor(() => {
        expect(screen.getByText('Failed: Invalid credentials')).toBeInTheDocument();
      });
    });
  });
});
