// src/pages/admin/components/AdminBrandManager.test.tsx
import React from 'react';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import toast from 'react-hot-toast';
import { AdminBrandManager } from './AdminBrandManager';
import * as apiClient from '../../../services/apiClient';
import { createMockBrand } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';

// After mocking, we can get a type-safe mocked version of the module.
// This allows us to use .mockResolvedValue, .mockRejectedValue, etc. on the functions.
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
const mockedApiClient = vi.mocked(apiClient);
const mockedToast = vi.mocked(toast, true);
const mockBrands = [
  createMockBrand({ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null }),
  createMockBrand({
    brand_id: 2,
    name: 'Compliments',
    store_name: 'Sobeys',
    logo_url: 'https://example.com/compliments.png',
  }),
];

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

  it('should render a loading state initially', () => {
    console.log('TEST START: should render a loading state initially');
    // Mock a promise that never resolves to keep the component in a loading state.
    console.log('TEST SETUP: Mocking fetchAllBrands with a non-resolving promise.');
    mockedApiClient.fetchAllBrands.mockReturnValue(new Promise(() => {}));

    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);

    console.log('TEST ASSERTION: Checking for the loading text.');
    expect(screen.getByText('Loading brands...')).toBeInTheDocument();
    console.log('TEST SUCCESS: Loading text is visible.');
    console.log('TEST END: should render a loading state initially');
  });

  it('should render an error message if fetching brands fails', async () => {
    console.log('TEST START: should render an error message if fetching brands fails');
    const errorMessage = 'Network Error';
    console.log(`TEST SETUP: Mocking fetchAllBrands to reject with: ${errorMessage}`);
    mockedApiClient.fetchAllBrands.mockRejectedValue(new Error('Network Error'));

    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);

    console.log('TEST ASSERTION: Waiting for error message to be displayed.');
    await waitFor(() => {
      expect(screen.getByText('Failed to load brands: Network Error')).toBeInTheDocument();
      console.log('TEST SUCCESS: Error message found in the document.');
    });
    console.log('TEST END: should render an error message if fetching brands fails');
  });

  it('should render the list of brands when data is fetched successfully', async () => {
    console.log('TEST START: should render the list of brands when data is fetched successfully');
    // Use mockImplementation to return a new Response object on each call,
    // preventing "Body has already been read" errors.
    console.log('TEST SETUP: Mocking fetchAllBrands to resolve with mockBrands.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );

    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);

    console.log('TEST ASSERTION: Waiting for brand list to render.');
    await waitFor(() => {
      expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
      expect(screen.getByText('No Frills')).toBeInTheDocument();
      expect(screen.getByText('(Sobeys)')).toBeInTheDocument();
      expect(screen.getByAltText('Compliments logo')).toBeInTheDocument();
      expect(screen.getByText('No Logo')).toBeInTheDocument();
      console.log('TEST SUCCESS: All brand elements found in the document.');
    });
    console.log('TEST END: should render the list of brands when data is fetched successfully');
  });

  it('should handle successful logo upload', async () => {
    console.log('TEST START: should handle successful logo upload');
    console.log('TEST SETUP: Mocking fetchAllBrands and uploadBrandLogo for success.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    mockedApiClient.uploadBrandLogo.mockImplementation(
      async () =>
        new Response(JSON.stringify({ logoUrl: 'https://example.com/new-logo.png' }), {
          status: 200,
        }),
    );
    mockedToast.loading.mockReturnValue('toast-1');

    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);
    console.log('TEST ACTION: Waiting for initial brands to render.');
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    // Use the new accessible label to find the correct input.
    const input = screen.getByLabelText('Upload logo for No Frills');

    console.log('TEST ACTION: Firing file change event on input for "No Frills".');
    fireEvent.change(input, { target: { files: [file] } });

    console.log('TEST ASSERTION: Waiting for upload to complete and UI to update.');
    await waitFor(() => {
      expect(mockedApiClient.uploadBrandLogo).toHaveBeenCalledWith(1, file);
      expect(mockedToast.loading).toHaveBeenCalledWith('Uploading logo...');
      expect(mockedToast.success).toHaveBeenCalledWith('Logo updated successfully!', {
        id: 'toast-1',
      });
      // Check if the UI updates with the new logo
      expect(screen.getByAltText('No Frills logo')).toHaveAttribute(
        'src',
        'https://example.com/new-logo.png',
      );
      console.log('TEST SUCCESS: All assertions for successful upload passed.');
    });
    console.log('TEST END: should handle successful logo upload');
  });

  it('should handle failed logo upload with a non-Error object', async () => {
    console.log('TEST START: should handle failed logo upload with a non-Error object');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    // Reject with a string instead of an Error object to test the fallback error handling
    mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error');
    mockedToast.loading.mockReturnValue('toast-non-error');

    renderWithProviders(<AdminBrandManager />);
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills');

    fireEvent.change(input, { target: { files: [file] } });

    await waitFor(() => {
      // This assertion verifies that the `String(e)` part of the catch block is executed.
      expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: A string error', {
        id: 'toast-non-error',
      });
    });
    console.log('TEST END: should handle failed logo upload with a non-Error object');
  });

  it('should handle failed logo upload', async () => {
    console.log('TEST START: should handle failed logo upload');
    console.log('TEST SETUP: Mocking fetchAllBrands for success and uploadBrandLogo for failure.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    mockedApiClient.uploadBrandLogo.mockRejectedValue(new Error('Upload failed'));
    mockedToast.loading.mockReturnValue('toast-2');

    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);
    console.log('TEST ACTION: Waiting for initial brands to render.');
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills');

    console.log('TEST ACTION: Firing file change event on input for "No Frills".');
    fireEvent.change(input, { target: { files: [file] } });

    console.log('TEST ASSERTION: Waiting for error toast to be called.');
    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Upload failed', {
        id: 'toast-2',
      });
      console.log('TEST SUCCESS: Error toast was called with the correct message.');
    });
    console.log('TEST END: should handle failed logo upload');
  });

  it('should show an error toast for invalid file type', async () => {
    console.log('TEST START: should show an error toast for invalid file type');
    console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);
    console.log('TEST ACTION: Waiting for initial brands to render.');
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['text'], 'document.txt', { type: 'text/plain' });
    const input = screen.getByLabelText('Upload logo for No Frills');

    console.log('TEST ACTION: Firing file change event with invalid file type.');
    fireEvent.change(input, { target: { files: [file] } });

    console.log('TEST ASSERTION: Waiting for validation error toast.');
    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith(
        'Invalid file type. Please upload a PNG, JPG, WEBP, or SVG.',
      );
      expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
      console.log('TEST SUCCESS: Validation toast shown and upload API not called.');
    });
    console.log('TEST END: should show an error toast for invalid file type');
  });

  it('should show an error toast for oversized file', async () => {
    console.log('TEST START: should show an error toast for oversized file');
    console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    console.log('TEST ACTION: Rendering AdminBrandManager component.');
    renderWithProviders(<AdminBrandManager />);
    console.log('TEST ACTION: Waiting for initial brands to render.');
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['a'.repeat(3 * 1024 * 1024)], 'large.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills');

    console.log('TEST ACTION: Firing file change event with oversized file.');
    fireEvent.change(input, { target: { files: [file] } });

    console.log('TEST ASSERTION: Waiting for size validation error toast.');
    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith('File is too large. Maximum size is 2MB.');
      expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
      console.log('TEST SUCCESS: Size validation toast shown and upload API not called.');
    });
    console.log('TEST END: should show an error toast for oversized file');
  });

  it('should show an error toast if upload fails with a non-ok response', async () => {
    console.log('TEST START: should handle non-ok response from upload API');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    // Mock a failed response (e.g., 400 Bad Request)
    mockedApiClient.uploadBrandLogo.mockResolvedValue(
      new Response('Invalid image format', { status: 400 }),
    );
    mockedToast.loading.mockReturnValue('toast-3');

    renderWithProviders(<AdminBrandManager />);
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills');

    fireEvent.change(input, { target: { files: [file] } });

    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Invalid image format', {
        id: 'toast-3',
      });
      console.log('TEST SUCCESS: Error toast shown for non-ok response.');
    });
    console.log('TEST END: should handle non-ok response from upload API');
  });

  it('should show an error toast if no file is selected', async () => {
    console.log('TEST START: should show an error toast if no file is selected');
    console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    renderWithProviders(<AdminBrandManager />);
    console.log('TEST ACTION: Waiting for initial brands to render.');
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const input = screen.getByLabelText('Upload logo for No Frills');
    // Simulate canceling the file picker by firing a change event with an empty file list.
    console.log('TEST ACTION: Firing file change event with an empty file list.');
    fireEvent.change(input, { target: { files: [] } });

    console.log('TEST ASSERTION: Waiting for the "no file selected" error toast.');
    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith('Please select a file to upload.');
      console.log('TEST SUCCESS: Error toast shown when no file is selected.');
    });
    console.log('TEST END: should show an error toast if no file is selected');
  });

  it('should render an empty table if no brands are found', async () => {
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify([]), { status: 200 }),
    );
    renderWithProviders(<AdminBrandManager />);

    await waitFor(() => {
      expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
      // Only the header row should be present
      expect(screen.getAllByRole('row')).toHaveLength(1);
    });
  });

  it('should use status code in error message if response body is empty on upload failure', async () => {
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    mockedApiClient.uploadBrandLogo.mockImplementation(
      async () => new Response(null, { status: 500, statusText: 'Internal Server Error' }),
    );
    mockedToast.loading.mockReturnValue('toast-fallback');

    renderWithProviders(<AdminBrandManager />);
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills');
    fireEvent.change(input, { target: { files: [file] } });

    await waitFor(() => {
      expect(mockedToast.error).toHaveBeenCalledWith(
        'Upload failed: Upload failed with status 500',
        { id: 'toast-fallback' },
      );
    });
  });

  it('should only update the target brand logo and leave others unchanged', async () => {
    mockedApiClient.fetchAllBrands.mockImplementation(
      async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
    );
    mockedApiClient.uploadBrandLogo.mockImplementation(
      async () => new Response(JSON.stringify({ logoUrl: 'new-logo.png' }), { status: 200 }),
    );
    mockedToast.loading.mockReturnValue('toast-opt');

    renderWithProviders(<AdminBrandManager />);
    await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());

    // Brand 1: No Frills (initially null logo)
    // Brand 2: Compliments (initially has logo)

    const file = new File(['logo'], 'logo.png', { type: 'image/png' });
    const input = screen.getByLabelText('Upload logo for No Frills'); // Brand 1
    fireEvent.change(input, { target: { files: [file] } });

    await waitFor(() => {
      // Brand 1 should have new logo
      expect(screen.getByAltText('No Frills logo')).toHaveAttribute('src', 'new-logo.png');
      // Brand 2 should still have original logo
      expect(screen.getByAltText('Compliments logo')).toHaveAttribute(
        'src',
        'https://example.com/compliments.png',
      );
    });
  });
});
